├── .gitignore ├── _dev └── cover.png ├── src ├── _assets │ ├── font.ttf │ ├── signal.png │ ├── TidyHand.ttf │ ├── paper_bg.png │ ├── schema_bg.png │ ├── audio │ │ ├── hack.ogg │ │ ├── menu.ogg │ │ ├── collision.wav │ │ ├── decoding_success.wav │ │ └── secret_unlocked.wav │ ├── blue_marker.png │ ├── decipher_bg.png │ ├── dial_button.png │ ├── hint_sticker.png │ ├── manual │ │ ├── rtfm.png │ │ ├── wat.png │ │ ├── cheetah.png │ │ ├── io_logs.png │ │ ├── scopes.png │ │ ├── shapes.png │ │ ├── collisions.png │ │ ├── hint_rot13.png │ │ ├── negation.png │ │ ├── ui_legend.png │ │ ├── hint_atbash.png │ │ ├── major_block.png │ │ ├── text_buffer.png │ │ ├── advanced_input.png │ │ ├── branching_info.png │ │ ├── cipher_pipeline.png │ │ ├── final_treasure.png │ │ ├── hidden_keybinds.png │ │ ├── value_inspector.png │ │ ├── conditions_vocab.png │ │ ├── output_predictor.png │ │ ├── optimized_decoding.png │ │ ├── full_completion_bonus.png │ │ ├── hacking_too_much_time.png │ │ ├── hint_polygraphic_ciphers.png │ │ ├── hint_substitution_ciphers.png │ │ ├── conditional_transformations.png │ │ └── hint_transposition_ciphers.png │ ├── onoff_toggle.png │ ├── sector_017.otf │ ├── terminal_bg.png │ ├── complete_mark.png │ ├── elements │ │ ├── pipe.png │ │ ├── elem_add.png │ │ ├── elem_if.png │ │ ├── elem_mux.png │ │ ├── elem_sub.png │ │ ├── angle_pipe.png │ │ ├── elem_ifnot.png │ │ ├── elem_input.png │ │ ├── elem_rot13.png │ │ ├── elem_add_even.png │ │ ├── elem_add_last.png │ │ ├── elem_add_odd.png │ │ ├── elem_atbash.png │ │ ├── elem_output.png │ │ ├── elem_repeater.png │ │ ├── elem_reverse.png │ │ ├── elem_sub_even.png │ │ ├── elem_sub_last.png │ │ ├── elem_sub_odd.png │ │ ├── elem_zigzag.png │ │ ├── pipe_connect2.png │ │ ├── special_pipe.png │ │ ├── elem_add_dotted.png │ │ ├── elem_add_first.png │ │ ├── elem_add_nowrap.png │ │ ├── elem_countdown0.png │ │ ├── elem_countdown1.png │ │ ├── elem_countdown2.png │ │ ├── elem_countdown3.png │ │ ├── elem_sub_first.png │ │ ├── elem_sub_nowrap.png │ │ ├── elem_add_butfirst.png │ │ ├── elem_atbash_first.png │ │ ├── elem_inv_repeater.png │ │ ├── elem_rot13_butlast.png │ │ ├── elem_rot13_first.png │ │ ├── elem_rotate_left.png │ │ ├── elem_rotate_right.png │ │ ├── elem_sub_butlast.png │ │ ├── elem_sub_undotted.png │ │ ├── elem_swap_halves.png │ │ ├── special_angle_pipe.png │ │ ├── elem_atbash_butlast.png │ │ ├── elem_hardshift_left.png │ │ ├── elem_hardshift_right.png │ │ ├── elem_rot13_butfirst.png │ │ ├── elem_polygraphic_atbash.png │ │ ├── elem_add_butfirst_dotted.png │ │ ├── elem_add_butfirst_nowrap.png │ │ ├── elem_rotate_left_butfirst.png │ │ └── elem_rotate_right_butfirst.png │ ├── ping_particle.png │ ├── pipeline_arrow.png │ ├── dial_button_arrow.png │ ├── chapter_select_outline.png │ ├── shader │ │ ├── handwriting.go │ │ └── video_distortion.go │ └── levels │ │ ├── bonus │ │ ├── lossy_conversion.json │ │ ├── double_zigzag.json │ │ ├── polygraphic_atbash.json │ │ ├── rumble.json │ │ ├── spellbook.json │ │ ├── pyramid.json │ │ └── clear_head.json │ │ └── story │ │ ├── hello_world.json │ │ ├── swap_shifter.json │ │ ├── rinse_repeat.json │ │ ├── branchless_encoder.json │ │ ├── dotmask.json │ │ ├── addsub_negation.json │ │ ├── atbash.json │ │ └── red_herring.json ├── _map_edit │ ├── hint.png │ └── settings.png ├── button_group.go ├── utils.go ├── sticker_node.go ├── assets.go ├── go.mod ├── signal_node.go ├── chapter_node.go ├── cmd │ └── mapcheck │ │ └── main.go ├── lcd_label.go ├── ping_effect_node.go ├── dial_button.go ├── schema_elem_node.go ├── chapter_select_controller.go ├── version.go ├── ui_theme.go ├── results_controller.go ├── content_management.go ├── text_ops.go ├── terminal_node.go ├── leveldata │ ├── schema.go │ └── template.go ├── main_menu_controller.go ├── manual_controller.go ├── options_controller.go ├── component_input.go ├── level_select_controller.go ├── level_generator.go ├── game_state.go └── custom_level_select_controller.go ├── _docs ├── running_custom_levels │ ├── level_select.png │ └── levels_folder.png ├── creating_custom_levels.md └── running_custom_levels.md ├── web └── index.html ├── BUILD.md ├── CREDITS.md ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /_dev/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/_dev/cover.png -------------------------------------------------------------------------------- /src/_assets/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/font.ttf -------------------------------------------------------------------------------- /src/_assets/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/signal.png -------------------------------------------------------------------------------- /src/_map_edit/hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_map_edit/hint.png -------------------------------------------------------------------------------- /src/_assets/TidyHand.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/TidyHand.ttf -------------------------------------------------------------------------------- /src/_assets/paper_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/paper_bg.png -------------------------------------------------------------------------------- /src/_assets/schema_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/schema_bg.png -------------------------------------------------------------------------------- /src/_assets/audio/hack.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/audio/hack.ogg -------------------------------------------------------------------------------- /src/_assets/audio/menu.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/audio/menu.ogg -------------------------------------------------------------------------------- /src/_assets/blue_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/blue_marker.png -------------------------------------------------------------------------------- /src/_assets/decipher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/decipher_bg.png -------------------------------------------------------------------------------- /src/_assets/dial_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/dial_button.png -------------------------------------------------------------------------------- /src/_assets/hint_sticker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/hint_sticker.png -------------------------------------------------------------------------------- /src/_assets/manual/rtfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/rtfm.png -------------------------------------------------------------------------------- /src/_assets/manual/wat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/wat.png -------------------------------------------------------------------------------- /src/_assets/onoff_toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/onoff_toggle.png -------------------------------------------------------------------------------- /src/_assets/sector_017.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/sector_017.otf -------------------------------------------------------------------------------- /src/_assets/terminal_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/terminal_bg.png -------------------------------------------------------------------------------- /src/_map_edit/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_map_edit/settings.png -------------------------------------------------------------------------------- /src/_assets/complete_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/complete_mark.png -------------------------------------------------------------------------------- /src/_assets/elements/pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/pipe.png -------------------------------------------------------------------------------- /src/_assets/manual/cheetah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/cheetah.png -------------------------------------------------------------------------------- /src/_assets/manual/io_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/io_logs.png -------------------------------------------------------------------------------- /src/_assets/manual/scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/scopes.png -------------------------------------------------------------------------------- /src/_assets/manual/shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/shapes.png -------------------------------------------------------------------------------- /src/_assets/ping_particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/ping_particle.png -------------------------------------------------------------------------------- /src/_assets/pipeline_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/pipeline_arrow.png -------------------------------------------------------------------------------- /src/_assets/audio/collision.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/audio/collision.wav -------------------------------------------------------------------------------- /src/_assets/dial_button_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/dial_button_arrow.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_if.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_if.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_mux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_mux.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub.png -------------------------------------------------------------------------------- /src/_assets/manual/collisions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/collisions.png -------------------------------------------------------------------------------- /src/_assets/manual/hint_rot13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hint_rot13.png -------------------------------------------------------------------------------- /src/_assets/manual/negation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/negation.png -------------------------------------------------------------------------------- /src/_assets/manual/ui_legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/ui_legend.png -------------------------------------------------------------------------------- /src/_assets/elements/angle_pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/angle_pipe.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_ifnot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_ifnot.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_input.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rot13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rot13.png -------------------------------------------------------------------------------- /src/_assets/manual/hint_atbash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hint_atbash.png -------------------------------------------------------------------------------- /src/_assets/manual/major_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/major_block.png -------------------------------------------------------------------------------- /src/_assets/manual/text_buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/text_buffer.png -------------------------------------------------------------------------------- /src/_assets/audio/decoding_success.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/audio/decoding_success.wav -------------------------------------------------------------------------------- /src/_assets/audio/secret_unlocked.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/audio/secret_unlocked.wav -------------------------------------------------------------------------------- /src/_assets/chapter_select_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/chapter_select_outline.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_even.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_even.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_last.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_odd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_odd.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_atbash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_atbash.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_output.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_repeater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_repeater.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_reverse.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_even.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_even.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_last.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_odd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_odd.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_zigzag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_zigzag.png -------------------------------------------------------------------------------- /src/_assets/elements/pipe_connect2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/pipe_connect2.png -------------------------------------------------------------------------------- /src/_assets/elements/special_pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/special_pipe.png -------------------------------------------------------------------------------- /src/_assets/manual/advanced_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/advanced_input.png -------------------------------------------------------------------------------- /src/_assets/manual/branching_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/branching_info.png -------------------------------------------------------------------------------- /src/_assets/manual/cipher_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/cipher_pipeline.png -------------------------------------------------------------------------------- /src/_assets/manual/final_treasure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/final_treasure.png -------------------------------------------------------------------------------- /src/_assets/manual/hidden_keybinds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hidden_keybinds.png -------------------------------------------------------------------------------- /src/_assets/manual/value_inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/value_inspector.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_dotted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_dotted.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_first.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_nowrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_nowrap.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_countdown0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_countdown0.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_countdown1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_countdown1.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_countdown2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_countdown2.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_countdown3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_countdown3.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_first.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_nowrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_nowrap.png -------------------------------------------------------------------------------- /src/_assets/manual/conditions_vocab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/conditions_vocab.png -------------------------------------------------------------------------------- /src/_assets/manual/output_predictor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/output_predictor.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_butfirst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_butfirst.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_atbash_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_atbash_first.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_inv_repeater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_inv_repeater.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rot13_butlast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rot13_butlast.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rot13_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rot13_first.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rotate_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rotate_left.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rotate_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rotate_right.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_butlast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_butlast.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_sub_undotted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_sub_undotted.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_swap_halves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_swap_halves.png -------------------------------------------------------------------------------- /src/_assets/elements/special_angle_pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/special_angle_pipe.png -------------------------------------------------------------------------------- /src/_assets/manual/optimized_decoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/optimized_decoding.png -------------------------------------------------------------------------------- /_docs/running_custom_levels/level_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/_docs/running_custom_levels/level_select.png -------------------------------------------------------------------------------- /_docs/running_custom_levels/levels_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/_docs/running_custom_levels/levels_folder.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_atbash_butlast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_atbash_butlast.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_hardshift_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_hardshift_left.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_hardshift_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_hardshift_right.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rot13_butfirst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rot13_butfirst.png -------------------------------------------------------------------------------- /src/_assets/manual/full_completion_bonus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/full_completion_bonus.png -------------------------------------------------------------------------------- /src/_assets/manual/hacking_too_much_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hacking_too_much_time.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_polygraphic_atbash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_polygraphic_atbash.png -------------------------------------------------------------------------------- /src/_assets/manual/hint_polygraphic_ciphers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hint_polygraphic_ciphers.png -------------------------------------------------------------------------------- /src/_assets/manual/hint_substitution_ciphers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hint_substitution_ciphers.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_butfirst_dotted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_butfirst_dotted.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_add_butfirst_nowrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_add_butfirst_nowrap.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rotate_left_butfirst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rotate_left_butfirst.png -------------------------------------------------------------------------------- /src/_assets/manual/conditional_transformations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/conditional_transformations.png -------------------------------------------------------------------------------- /src/_assets/manual/hint_transposition_ciphers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/manual/hint_transposition_ciphers.png -------------------------------------------------------------------------------- /src/_assets/elements/elem_rotate_right_butfirst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasilyte/decipherism-game/HEAD/src/_assets/elements/elem_rotate_right_butfirst.png -------------------------------------------------------------------------------- /_docs/creating_custom_levels.md: -------------------------------------------------------------------------------- 1 | # Creating Custom Levels 2 | 3 | The levels for this game are created using an awesome [Tiled](https://www.mapeditor.org/) program. It's a general-purpose map editor. 4 | 5 | The easiest way to understand how to create your own level is to watch this video: TODO. 6 | 7 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/button_group.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/quasilyte/ge/ui" 4 | 5 | type buttonGroup struct { 6 | buttons []*ui.Button 7 | } 8 | 9 | func (g *buttonGroup) AddButton(b *ui.Button) { 10 | g.buttons = append(g.buttons, b) 11 | } 12 | 13 | func (g *buttonGroup) FocusFirst() { 14 | if len(g.buttons) == 0 { 15 | return 16 | } 17 | g.buttons[0].SetFocus(true) 18 | } 19 | 20 | func (g *buttonGroup) Connect(root *ui.Root) { 21 | if len(g.buttons) < 2 { 22 | return 23 | } 24 | for i := 0; i < len(g.buttons)-1; i++ { 25 | root.ConnectInputs(g.buttons[i+0], g.buttons[i+1]) 26 | } 27 | root.ConnectInputs(g.buttons[len(g.buttons)-1], g.buttons[0]) 28 | } 29 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Building from Source 2 | 3 | This should be enough to build the game from sources: 4 | 5 | ```bash 6 | cd src 7 | go build -o ../bin/decipherism . 8 | ``` 9 | 10 | > You will need a go 1.18+ in order to build this game. 11 | 12 | If you want to build a game for a different platform, use Go cross-compilation: 13 | 14 | ```bash 15 | GOOS=windows go build -o ../bin/decipherism.exe . 16 | ``` 17 | 18 | To build a game for wasm (browser): 19 | 20 | ```bash 21 | GOOS=js GOARCH=wasm go build -o ../web/main.wasm . 22 | ``` 23 | 24 | After that, a `web` folder will contain 3 files: 25 | 26 | * index.html 27 | * wasm_exec.js 28 | * main.wasm 29 | 30 | Put these files into a single archive to create an itch-io uploadable bundle. 31 | 32 | This game is tested on these targets: 33 | 34 | * windows/amd64 35 | * linux/amd64 36 | * js/wasm 37 | -------------------------------------------------------------------------------- /src/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "hash/fnv" 5 | 6 | "github.com/quasilyte/decipherism-game/leveldata" 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/ge/tiled" 9 | ) 10 | 11 | func loadLevelTemplate(scene *ge.Scene, levelData []byte) (*leveldata.SchemaTemplate, error) { 12 | tileset, err := tiled.UnmarshalTileset(scene.LoadRaw(RawComponentSchemaTilesetJSON).Data) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return leveldata.LoadLevelTemplate(tileset, levelData) 17 | } 18 | 19 | func fnvhash(b []byte) uint64 { 20 | hash := fnv.New64a() 21 | hash.Write(b) 22 | return hash.Sum64() 23 | } 24 | 25 | func volumeMultiplier(level int) float64 { 26 | switch level { 27 | case 1: 28 | return 0.05 29 | case 2: 30 | return 0.10 31 | case 3: 32 | return 0.3 33 | case 4: 34 | return 0.55 35 | case 5: 36 | return 0.8 37 | default: 38 | return 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # CREDITS 2 | 3 | ## Software 4 | 5 | * [Tiled](https://thorbjorn.itch.io/tiled) was used to create the game levels 6 | 7 | ## Fonts 8 | 9 | * https://www.dafont.com/sector-017.font by Neoqueto 10 | * https://www.dafont.com/electronic-highway-sign.font by Ash Pikachu Font 11 | * https://www.dafont.com/tidy-hand.font by Sean Johnson 12 | 13 | ## Sound effects 14 | 15 | All sound effects are generated by [sfxr](https://www.drpetter.se/project_sfxr.html) (by drpetter). 16 | 17 | ## Music 18 | 19 | Kalte Ohren by Alex (c) copyright 2019 Licensed under a Creative Commons Attribution (3.0) license. http://dig.ccmixter.org/files/AlexBeroza/59612 Ft: starfrosch & Jerry Spoon 20 | 21 | Drive by Alex (c) copyright 2013 Licensed under a Creative Commons Attribution (3.0) license. http://dig.ccmixter.org/files/AlexBeroza/43098 Ft: cdk & Darryl J 22 | 23 | ## Special thanks 24 | 25 | * https://www.twitch.tv/babytigeronthesunflower 26 | -------------------------------------------------------------------------------- /src/_assets/shader/handwriting.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | func Fragment(pos vec4, texCoord vec2, _ vec4) vec4 { 7 | c := imageSrc0At(texCoord) 8 | 9 | pixSize := imageSrcTextureSize() 10 | originTexPos, _ := imageSrcRegionOnTexture() 11 | actualTexPos := vec2(texCoord.x-originTexPos.x, texCoord.y-originTexPos.y) 12 | actualPixPos := actualTexPos * pixSize 13 | if c[3] != 0.0 { 14 | p := actualPixPos 15 | posHash := int(actualPixPos.x+actualPixPos.y) * int(actualPixPos.y*5) 16 | state := posHash % 15 17 | if state == int(1) { 18 | p.x += 1.0 19 | } else if state == int(2) { 20 | p.x -= 1.0 21 | } else if state == int(3) { 22 | p.y += 1.0 23 | } else if state == int(4) { 24 | p.y -= 1.0 25 | } else { 26 | return c 27 | } 28 | colorMultiplier := vec4(0.95, 0.95, 0.95, 0.95) 29 | return imageSrc0At(p/pixSize+originTexPos) * colorMultiplier 30 | } 31 | 32 | return c 33 | } 34 | -------------------------------------------------------------------------------- /src/sticker_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quasilyte/ge" 5 | "github.com/quasilyte/gmath" 6 | ) 7 | 8 | type stickerNode struct { 9 | pos gmath.Vec 10 | 11 | sprite *ge.Sprite 12 | label *ge.Label 13 | 14 | text string 15 | } 16 | 17 | func newStickerNode(pos gmath.Vec, text string) *stickerNode { 18 | return &stickerNode{ 19 | pos: pos, 20 | text: text, 21 | } 22 | } 23 | 24 | func (s *stickerNode) Init(scene *ge.Scene) { 25 | s.sprite = scene.NewSprite(ImageHintSticker) 26 | s.sprite.Centered = false 27 | s.sprite.Pos.Base = &s.pos 28 | scene.AddGraphics(s.sprite) 29 | 30 | s.label = scene.NewLabel(FontHandwrittenSmall) 31 | s.label.Pos.Base = &s.pos 32 | s.label.Pos.Offset = gmath.Vec{X: 34, Y: 72} 33 | s.label.Text = s.text 34 | s.label.ColorScale.SetRGBA(30, 30, 60, 220) 35 | scene.AddGraphics(s.label) 36 | } 37 | 38 | func (s *stickerNode) IsDisposed() bool { return false } 39 | 40 | func (s *stickerNode) Update(delta float64) {} 41 | -------------------------------------------------------------------------------- /src/assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | resource "github.com/quasilyte/ebitengine-resource" 7 | "github.com/quasilyte/ge" 8 | ) 9 | 10 | func prepareAssets(ctx *ge.Context) { 11 | theStoryModeMap.levels = make(map[string]storyModeLevel) 12 | resourceID := rawLastID + 1 13 | resourceID = loadLevelsData(ctx, resourceID, "levels/story") 14 | resourceID = loadLevelsData(ctx, resourceID, "levels/bonus") 15 | } 16 | 17 | func loadLevelsData(ctx *ge.Context, idSeq resource.RawID, dirname string) resource.RawID { 18 | levels, err := gameAssets.ReadDir("_assets/" + dirname) 19 | if err != nil { 20 | panic(err) 21 | } 22 | for _, f := range levels { 23 | shortName := strings.TrimSuffix(f.Name(), ".json") 24 | theStoryModeMap.levels[shortName] = storyModeLevel{ 25 | name: shortName, 26 | id: idSeq, 27 | } 28 | ctx.Loader.RawRegistry.Set(idSeq, resource.RawInfo{ 29 | Path: dirname + "/" + f.Name(), 30 | }) 31 | ctx.Loader.LoadRaw(idSeq) 32 | idSeq++ 33 | } 34 | return idSeq 35 | } 36 | -------------------------------------------------------------------------------- /_docs/running_custom_levels.md: -------------------------------------------------------------------------------- 1 | # Running Custom Levels 2 | 3 | To run a custom level, you first need to either [create it](creating_custom_levels.md) or download it. 4 | 5 | Let's suppose that you have a level file named `example.json` (level files are always a single JSON file). 6 | 7 | The easiest way to make the game locate that file is to put it next to the game, like so: 8 | 9 | ``` 10 | * decipherism.exe 11 | * levels/ 12 | * example.json 13 | ``` 14 | 15 | ![](running_custom_levels/levels_folder.png) 16 | 17 | So, you first create a "levels" folder next to the executable and then you put all custom levels in there. 18 | 19 | If you don't want to store the levels next to the game executable, you can set the `$DECIPHERISM_DATA` environment variable. It should point to the directory containing the `levels/` folder. 20 | 21 | To run a custom level, click "Run a custom simulation" main menu button. You'll see a screen listing the custom levels found by the game. 22 | 23 | ![](running_custom_levels/level_select.png) 24 | 25 | Click a level from that list to run it. 26 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quasilyte/decipherism-game 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.4.16 7 | github.com/quasilyte/ge v0.0.0-20230202151823-9fbdf6e7770e 8 | github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.1.1 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/hajimehoshi/file2byteslice v1.0.0 // indirect 15 | github.com/hajimehoshi/oto/v2 v2.3.1 // indirect 16 | github.com/jezek/xgb v1.1.0 // indirect 17 | github.com/jfreymuth/oggvorbis v1.0.4 // indirect 18 | github.com/jfreymuth/vorbis v1.0.2 // indirect 19 | github.com/quasilyte/ebitengine-resource v0.5.1-0.20230131121810-c18be064e3ae // indirect 20 | golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e // indirect 21 | golang.org/x/exp/shiny v0.0.0-20230118134722-a68e582fa157 // indirect 22 | golang.org/x/image v0.3.0 // indirect 23 | golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect 24 | golang.org/x/sys v0.4.0 // indirect 25 | golang.org/x/text v0.6.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /src/signal_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quasilyte/ge" 5 | "github.com/quasilyte/ge/gesignal" 6 | "github.com/quasilyte/gmath" 7 | ) 8 | 9 | type signalNode struct { 10 | pos gmath.Vec 11 | sprite *ge.Sprite 12 | dst gmath.Vec 13 | speed float64 14 | 15 | EventDestinationReached gesignal.Event[*signalNode] 16 | } 17 | 18 | func newSignalNode(pos gmath.Vec) *signalNode { 19 | return &signalNode{ 20 | pos: pos, 21 | speed: 160, 22 | } 23 | } 24 | 25 | func (s *signalNode) Init(scene *ge.Scene) { 26 | s.sprite = scene.NewSprite(ImageSignal) 27 | s.sprite.Pos.Base = &s.pos 28 | scene.AddGraphics(s.sprite) 29 | } 30 | 31 | func (s *signalNode) IsDisposed() bool { 32 | return s.sprite.IsDisposed() 33 | } 34 | 35 | func (s *signalNode) Dispose() { 36 | s.sprite.Dispose() 37 | } 38 | 39 | func (s *signalNode) Update(delta float64) { 40 | if s.dst.IsZero() { 41 | return 42 | } 43 | 44 | step := s.speed * delta 45 | if s.pos.DistanceTo(s.dst) < step { 46 | s.pos = s.dst 47 | s.dst = gmath.Vec{} 48 | s.EventDestinationReached.Emit(s) 49 | } else { 50 | s.pos = s.pos.MoveTowards(s.dst, step) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Iskander (Alex) Sharipov / quasilyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/chapter_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type chapterNode struct { 11 | pos gmath.Vec 12 | name string 13 | rotation gmath.Rad 14 | available bool 15 | completed bool 16 | } 17 | 18 | func newChapterNode(pos gmath.Vec, name string, available, completed bool) *chapterNode { 19 | return &chapterNode{ 20 | pos: pos, 21 | name: name, 22 | available: available, 23 | completed: completed, 24 | } 25 | } 26 | 27 | func (n *chapterNode) Init(scene *ge.Scene) { 28 | if n.available { 29 | s := scene.NewSprite(ImageBlueMarker) 30 | s.Centered = false 31 | s.Pos.Base = &n.pos 32 | if scene.Rand().Bool() { 33 | n.rotation = math.Pi 34 | s.Pos.Offset = gmath.Vec{X: -42.5 * 4, Y: -42.5 * 4} 35 | } 36 | s.Rotation = &n.rotation 37 | scene.AddGraphics(s) 38 | } 39 | if n.completed { 40 | s := scene.NewSprite(ImageCompleteMark) 41 | s.Centered = false 42 | s.Pos.Base = &n.pos 43 | s.Pos.Offset = gmath.Vec{X: -42.5, Y: 42.5 * 3} 44 | scene.AddGraphics(s) 45 | } 46 | } 47 | 48 | func (n *chapterNode) IsDisposed() bool { return false } 49 | 50 | func (n *chapterNode) Update(delta float64) {} 51 | -------------------------------------------------------------------------------- /src/_assets/shader/video_distortion.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | var Tick float 7 | var Seed float 8 | 9 | func Fragment(pos vec4, texCoord vec2, _ vec4) vec4 { 10 | c := imageSrc0At(texCoord) 11 | 12 | colorMultiplier := vec4(1, 1, 1, 1) 13 | if c[3] != 0.0 { 14 | if int(pos.y+Tick)%4 != int(0) { 15 | colorMultiplier = vec4(0.6, 0.6, 0.6, 0.6) 16 | } 17 | } 18 | 19 | pixSize := imageSrcTextureSize() 20 | originTexPos, _ := imageSrcRegionOnTexture() 21 | actualTexPos := vec2(texCoord.x-originTexPos.x, texCoord.y-originTexPos.y) 22 | actualPixPos := actualTexPos * pixSize 23 | intSeed := int(Seed) 24 | if c[3] != 0.0 { 25 | p := actualPixPos 26 | pixelOffset := int(actualPixPos.x) + int(actualPixPos.y*pixSize.x) 27 | seedMod := pixelOffset % intSeed 28 | pixelOffset += seedMod 29 | 30 | posHash := pixelOffset + intSeed 31 | dir := posHash % 5 32 | dist := 1.0 33 | if seedMod == int(0) { 34 | dist = 2.0 35 | } 36 | if dir == int(1) { 37 | p.x += dist 38 | } else if dir == int(2) { 39 | p.x -= dist 40 | } else if dir == int(3) { 41 | p.y += dist 42 | } else if dir == int(4) { 43 | p.y -= dist 44 | } 45 | return imageSrc0At(p/pixSize+originTexPos) * colorMultiplier 46 | } 47 | 48 | return c 49 | } 50 | -------------------------------------------------------------------------------- /src/cmd/mapcheck/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/quasilyte/decipherism-game/leveldata" 10 | "github.com/quasilyte/ge/tiled" 11 | ) 12 | 13 | func main() { 14 | log.SetFlags(0) 15 | 16 | tilesetPath := flag.String("tileset", "", 17 | `path to a schemas.tsj file`) 18 | flag.Parse() 19 | 20 | if *tilesetPath == "" { 21 | log.Fatal("--tileset can't be empty") 22 | } 23 | if len(flag.Args()) == 0 { 24 | log.Fatal("expected at least 1 positional argument") 25 | } 26 | 27 | tilesetData, err := os.ReadFile(*tilesetPath) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | tileset, err := tiled.UnmarshalTileset(tilesetData) 33 | if err != nil { 34 | log.Fatalf("[ERROR] decode tileset file: %v", err) 35 | } 36 | 37 | hasErrors := false 38 | for _, filename := range flag.Args() { 39 | err := checkFile(tileset, filename) 40 | if err != nil { 41 | hasErrors = true 42 | fmt.Fprintf(os.Stderr, "%q: %v\n", filename, err) 43 | } 44 | } 45 | 46 | if hasErrors { 47 | os.Exit(1) 48 | } else { 49 | fmt.Printf("[OK] all files are good (checked %d files)\n", len(flag.Args())) 50 | } 51 | } 52 | 53 | func checkFile(tileset *tiled.Tileset, filename string) error { 54 | levelData, err := os.ReadFile(filename) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return leveldata.ValidateLevelData(tileset, levelData) 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cover](_dev/cover.png) 2 | 3 | ## Overview 4 | 5 | A puzzle game where you solve the encoding machine ciphers. 6 | 7 | The key features are: 8 | 9 | * Puzzles that can be solved in more than one way 10 | * Complex cross-level secrets 11 | * A stylish in-game manual 12 | * Immersive interface 13 | 14 | This game natively works on Windows, Linux and in browsers (wasm build). You can download the native binary or play it in browser at [itch.io](https://quasilyte.itch.io/decipherism). 15 | 16 | ## Game Development Details 17 | 18 | This game is a [Game Off 2022](https://itch.io/jam/game-off-2022) game jam submission. 19 | 20 | The game jam theme is **cliche**. Therefore, you're hacking a ~~Penth~~ Hexagon 21 | using some retro-style hacking device with bright green consoles. 22 | 23 | The game created using [Ebitengine](https://github.com/hajimehoshi/ebiten/). 24 | It's proper home is [quasilyte/ge](https://github.com/quasilyte/ge/) repository, 25 | but since it's a requirement to create a separate per-game repository, here we are. 26 | 27 | ## Lincense and Credits 28 | 29 | Everything is licensed under the MIT license, unless there is an entry in CREDITS for that asset. 30 | If there is an entry in [CREDITS](CREDITS.md) file, it overrides the default MIT licensing. 31 | 32 | The game itself doesn't have a credits screen, but all places where you can download this 33 | game should include a supplementary CREDITS information. 34 | -------------------------------------------------------------------------------- /src/lcd_label.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type lcdLabel struct { 11 | pos gmath.Vec 12 | text string 13 | clr color.RGBA 14 | label *ge.Label 15 | labelBg *ge.Rect 16 | } 17 | 18 | var ( 19 | defaultLCDColor = ge.RGB(0x2a9535) 20 | collisionLCDColor = ge.RGB(0xa32828) 21 | successLCDColor = ge.RGB(0xcec844) 22 | ) 23 | 24 | func newLCDLabel(pos gmath.Vec, clr color.RGBA, text string) *lcdLabel { 25 | return &lcdLabel{pos: pos, text: text, clr: clr} 26 | } 27 | 28 | func (l *lcdLabel) Init(scene *ge.Scene) { 29 | l.labelBg = ge.NewRect(scene.Context(), 328, 64) 30 | l.labelBg.Centered = false 31 | l.labelBg.Pos.Base = &l.pos 32 | l.labelBg.OutlineWidth = 4 33 | l.labelBg.FillColorScale.SetColor(ge.RGB(0x151917)) 34 | l.labelBg.OutlineColorScale.SetRGBA(0x14, 0x12, 0x1b, 80) 35 | scene.AddGraphics(l.labelBg) 36 | 37 | l.label = scene.NewLabel(FontLCDSmall) 38 | l.label.Text = l.text 39 | l.label.Pos = l.labelBg.Pos 40 | l.label.ColorScale.SetColor(l.clr) 41 | l.label.Width = 320 42 | l.label.Height = 64 43 | l.label.AlignHorizontal = ge.AlignHorizontalCenter 44 | l.label.AlignVertical = ge.AlignVerticalCenter 45 | scene.AddGraphics(l.label) 46 | } 47 | 48 | func (l *lcdLabel) IsDisposed() bool { return false } 49 | 50 | func (l *lcdLabel) SetColor(clr color.RGBA) { 51 | l.label.ColorScale.SetColor(clr) 52 | } 53 | 54 | func (l *lcdLabel) Update(delta float64) { 55 | l.label.Text = l.text 56 | } 57 | -------------------------------------------------------------------------------- /src/ping_effect_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type pingEffectParticle struct { 11 | velocity gmath.Vec 12 | pos gmath.Vec 13 | rotation gmath.Rad 14 | sprite *ge.Sprite 15 | } 16 | 17 | type pingEffectNode struct { 18 | pos gmath.Vec 19 | c ge.ColorScale 20 | particles [4]pingEffectParticle 21 | } 22 | 23 | func newPingEffectNode(pos gmath.Vec, c ge.ColorScale) *pingEffectNode { 24 | return &pingEffectNode{ 25 | pos: pos, 26 | c: c, 27 | } 28 | } 29 | 30 | func (e *pingEffectNode) Init(scene *ge.Scene) { 31 | rotation := gmath.Rad(0) 32 | for i := range e.particles { 33 | p := &e.particles[i] 34 | p.rotation = rotation 35 | p.pos = e.pos.MoveInDirection(32, rotation-(math.Pi/4)) 36 | p.velocity = gmath.RadToVec(rotation - (math.Pi / 4)).Mulf(20) 37 | p.sprite = scene.NewSprite(ImagePingParticle) 38 | p.sprite.Rotation = &p.rotation 39 | p.sprite.Pos.Base = &p.pos 40 | p.sprite.SetColorScale(e.c) 41 | scene.AddGraphics(p.sprite) 42 | rotation += math.Pi / 2 43 | } 44 | } 45 | 46 | func (e *pingEffectNode) IsDisposed() bool { 47 | return e.particles[0].sprite.IsDisposed() 48 | } 49 | 50 | func (e *pingEffectNode) Update(delta float64) { 51 | alpha := e.particles[0].sprite.GetAlpha() 52 | if alpha < 0.1 { 53 | for _, p := range e.particles { 54 | p.sprite.Dispose() 55 | } 56 | return 57 | } 58 | for i := range e.particles { 59 | p := &e.particles[i] 60 | p.sprite.SetAlpha(alpha - float32(delta*2)) 61 | p.pos = p.pos.Add(p.velocity.Mulf(delta)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/dial_button.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/ge/gesignal" 8 | "github.com/quasilyte/ge/ui" 9 | "github.com/quasilyte/gmath" 10 | ) 11 | 12 | type dialButton struct { 13 | pos gmath.Vec 14 | centerPos gmath.Vec 15 | 16 | body *ge.Sprite 17 | arrow *ge.Sprite 18 | arrowRotation gmath.Rad 19 | arrowStep gmath.Rad 20 | numStates int 21 | state int 22 | 23 | root *ui.Root 24 | button *ui.Button 25 | 26 | EventActivated gesignal.Event[int] 27 | } 28 | 29 | func newDialButton(root *ui.Root, pos gmath.Vec, numStates int) *dialButton { 30 | return &dialButton{ 31 | pos: pos, 32 | centerPos: pos.Add(gmath.Vec{X: 56, Y: 50}), 33 | root: root, 34 | numStates: numStates, 35 | arrowStep: math.Pi / gmath.Rad(numStates-1), 36 | } 37 | } 38 | 39 | func (b *dialButton) Init(scene *ge.Scene) { 40 | b.button = b.root.NewButton(invisibleButtonStyle.Resized(108, 108)) 41 | b.button.Pos.Base = &b.pos 42 | b.button.EventActivated.Connect(nil, func(_ *ui.Button) { 43 | b.state++ 44 | if b.state >= b.numStates { 45 | b.state = 0 46 | b.arrowRotation = math.Pi 47 | } else { 48 | b.arrowRotation += b.arrowStep 49 | } 50 | b.EventActivated.Emit(b.state) 51 | }) 52 | scene.AddObject(b.button) 53 | 54 | b.body = scene.NewSprite(ImageDialButton) 55 | b.body.Pos.Base = &b.centerPos 56 | scene.AddGraphics(b.body) 57 | 58 | b.arrowRotation = math.Pi + (b.arrowStep * gmath.Rad(b.state)) 59 | 60 | b.arrow = scene.NewSprite(ImageDialButtonArrow) 61 | b.arrow.Pos.Base = &b.centerPos 62 | b.arrow.Rotation = &b.arrowRotation 63 | scene.AddGraphics(b.arrow) 64 | } 65 | 66 | func (b *dialButton) IsDisposed() bool { 67 | return false 68 | } 69 | 70 | func (b *dialButton) Update(delta float64) { 71 | } 72 | -------------------------------------------------------------------------------- /src/schema_elem_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quasilyte/decipherism-game/leveldata" 5 | resource "github.com/quasilyte/ebitengine-resource" 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type schemaElemNode struct { 11 | data *leveldata.SchemaElem 12 | 13 | rotation gmath.Rad 14 | 15 | shaderEnabled bool 16 | shaderTick float64 17 | shaderTickDelay float64 18 | shaderSeed int 19 | shaderStep int 20 | 21 | sprite *ge.Sprite 22 | } 23 | 24 | func newSchemaElemNode(data *leveldata.SchemaElem, shaderEnabled bool) *schemaElemNode { 25 | return &schemaElemNode{ 26 | data: data, 27 | rotation: data.Rotation, 28 | shaderEnabled: shaderEnabled, 29 | } 30 | } 31 | 32 | func (n *schemaElemNode) Init(scene *ge.Scene) { 33 | imageID := resource.ImageID(n.data.TileClassID) + componentSchemaImageOffset + 1 34 | n.sprite = scene.NewSprite(imageID) 35 | n.sprite.Pos.Base = &n.data.Pos 36 | n.sprite.Rotation = &n.rotation 37 | if extra, ok := n.data.ExtraData.(*leveldata.AngleElemExtra); ok { 38 | n.sprite.FlipHorizontal = extra.FlipHorizontally 39 | } 40 | scene.AddGraphics(n.sprite) 41 | if n.shaderEnabled { 42 | n.sprite.Shader = scene.NewShader(ShaderVideoDistortion) 43 | n.shaderStep = scene.Rand().IntRange(1, 4) 44 | n.sprite.Shader.SetIntValue("Seed", scene.Rand().IntRange(10, 500)) 45 | n.shaderTickDelay = 0.2 46 | n.shaderTick = float64(scene.Rand().IntRange(0, 2)) 47 | } 48 | } 49 | 50 | func (n *schemaElemNode) IsDisposed() bool { 51 | return n.sprite.IsDisposed() 52 | } 53 | 54 | func (n *schemaElemNode) Update(delta float64) { 55 | if !n.shaderEnabled { 56 | return 57 | } 58 | n.shaderTickDelay -= delta 59 | if n.shaderTickDelay <= 0 { 60 | n.shaderTickDelay = 0.3 61 | n.shaderTick += 1.0 62 | if n.shaderTick > 3.0 { 63 | n.shaderTick = 0 64 | } 65 | n.shaderSeed += n.shaderStep 66 | if n.shaderSeed > 1000 { 67 | n.shaderSeed = 10 68 | } 69 | n.sprite.Shader.SetIntValue("Seed", n.shaderSeed+1) 70 | n.sprite.Shader.SetFloatValue("Tick", n.shaderTick) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/chapter_select_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quasilyte/ge" 5 | "github.com/quasilyte/ge/ui" 6 | "github.com/quasilyte/ge/xslices" 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type chapterSelectController struct { 11 | scene *ge.Scene 12 | gameState *gameState 13 | } 14 | 15 | func newChapterSelectController(s *gameState) *chapterSelectController { 16 | return &chapterSelectController{gameState: s} 17 | } 18 | 19 | func (c *chapterSelectController) Init(scene *ge.Scene) { 20 | c.scene = scene 21 | 22 | bg := scene.NewSprite(ImagePaperBg) 23 | bg.Centered = false 24 | bg.FlipHorizontal = true 25 | scene.AddGraphics(bg) 26 | 27 | uiRoot := ui.NewRoot(c.scene.Context(), c.gameState.input) 28 | uiRoot.ActivationAction = ActionMenuConfirm 29 | c.scene.AddObject(uiRoot) 30 | 31 | nodeOffset := gmath.Vec{X: 112 + (42 * 4), Y: 36 + (42.5 * 3)} 32 | content := calculateContentStatus(c.gameState) 33 | for i := range theStoryModeMap.chapters { 34 | chapter := &theStoryModeMap.chapters[i] 35 | chapterStatus := c.gameState.GetChapterCompletionData(chapter) 36 | available := xslices.Contains(content.chapters, chapter.name) 37 | pos := nodeOffset.Add(gmath.Vec{X: 42 * (chapter.gridPos.X * 7), Y: 42 * (chapter.gridPos.Y * 7)}) 38 | if !available { 39 | continue 40 | } 41 | n := newChapterNode(pos, chapter.name, available, chapterStatus.fullyCompleted) 42 | scene.AddObject(n) 43 | 44 | button := uiRoot.NewButton(labelButtonStyle.Resized(42*4, 42.5*4)) 45 | button.Pos.Offset = pos 46 | button.Text = chapter.label 47 | button.EventActivated.Connect(nil, func(_ *ui.Button) { 48 | c.gameState.chapter = chapter 49 | c.scene.Context().ChangeScene(newLevelSelectController(c.gameState)) 50 | }) 51 | c.scene.AddObject(button) 52 | } 53 | 54 | outline := scene.NewSprite(ImageChapterSelectOutline) 55 | outline.Centered = false 56 | scene.AddGraphics(outline) 57 | } 58 | 59 | func (c *chapterSelectController) Update(delta float64) { 60 | if c.gameState.input.ActionIsJustPressed(ActionLeave) { 61 | c.leave() 62 | return 63 | } 64 | } 65 | 66 | func (c *chapterSelectController) leave() { 67 | c.scene.Context().ChangeScene(newMainMenuController(c.gameState)) 68 | } 69 | -------------------------------------------------------------------------------- /src/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // TODO: 4 | // * reduce the number of encoding collision keywords 5 | // 6 | // Build 2. 7 | // -- Added the encoding collision sfx 8 | // -- Added the encoding collision manual entry 9 | // -- Renamed "Access Keywords" to "Encoded Keywords" 10 | // -- Fixed ctrl modifier in wasm builds 11 | // 12 | // Build 3. 13 | // > Bug fixes: 14 | // -- Fixed io logs text alignment 15 | // -- Fixed a compound keyword encoded collision bug 16 | // -- Fixed invisible cursor at 10 input letters 17 | // > Content modifications: 18 | // -- Changed the final compound keyword to "cloudburst" 19 | // -- Rebalanced some levels 20 | // -- Fixed a few typos (in various places!) 21 | // -- Replaced some slang to be more player-friendly 22 | // -- Changed the decipher screen music 23 | // > New content: 24 | // -- Added a polygraphic ciphers manual page 25 | // -- Added a conditional transformations manual page 26 | // -- Added a couple of new levels 27 | // -- Added menu music 28 | // 29 | // Build 4. 30 | // > Other: 31 | // -- Made sound levels configurable 32 | // > Content modifications: 33 | // -- Rebalanced some levels 34 | // > New content: 35 | // -- Added a conditions vocab manual page 36 | // -- Added a "binary tree" level 37 | // -- Added a "conveyor" level 38 | // 39 | // Build 5. 40 | // > Bug fixes: 41 | // -- Second checkmark near the bonus levels (if they were completed and moved from the story levels) 42 | // > Content modifications: 43 | // -- A "switch" level now modifies words longer than 5 too 44 | // -- Done some more manual pages proof reading 45 | // 46 | // Build 6. 47 | // > Gameplay: 48 | // -- Increased the game speed scale 49 | // -- Encoding collisions results in red output text 50 | // -- Encoding success results in gold output text 51 | // -- "go to manual" action after clearing the level 52 | // -- Added help stickers to some starting levels 53 | // -- New "custom simulations" mode 54 | // > Content modifications: 55 | // -- Some balance changes 56 | // 57 | // Build 7: 58 | // > UX: 59 | // -- Can now type in letters while holding "shift" (also fixes capslock issue) 60 | // -- Use 'keyname' notation instead [keyname] inside results screen (more readable) 61 | // > Other: 62 | // -- Improved custom levels selection screen 63 | const buildVersion = "7" 64 | -------------------------------------------------------------------------------- /src/ui_theme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quasilyte/ge" 5 | "github.com/quasilyte/ge/ui" 6 | ) 7 | 8 | var invisibleButtonStyle ui.ButtonStyle 9 | var outlineButtonStyle ui.ButtonStyle 10 | var labelButtonStyle ui.ButtonStyle 11 | var optionsButtonStyle ui.ButtonStyle 12 | 13 | func init() { 14 | optionsButtonStyle = ui.DefaultButtonStyle() 15 | optionsButtonStyle.Font = FontLCDNormal 16 | optionsButtonStyle.TextColor.SetColor(defaultLCDColor) 17 | optionsButtonStyle.BorderWidth = 4 18 | optionsButtonStyle.BackgroundColor.A = 0 19 | optionsButtonStyle.BorderColor.SetColor(defaultLCDColor) 20 | optionsButtonStyle.FocusedBackgroundColor = optionsButtonStyle.BackgroundColor 21 | optionsButtonStyle.FocusedBorderColor = optionsButtonStyle.BorderColor 22 | optionsButtonStyle.FocusedTextColor.SetColor(ge.RGB(0xcec844)) 23 | optionsButtonStyle.DisabledBackgroundColor = optionsButtonStyle.BackgroundColor 24 | optionsButtonStyle.DisabledBorderColor = optionsButtonStyle.BorderColor 25 | optionsButtonStyle.DisabledTextColor = optionsButtonStyle.TextColor 26 | 27 | invisibleButtonStyle = ui.DefaultButtonStyle() 28 | invisibleButtonStyle.BorderWidth = 0 29 | invisibleButtonStyle.BackgroundColor.A = 0 30 | invisibleButtonStyle.FocusedBackgroundColor.A = 0 31 | invisibleButtonStyle.DisabledBackgroundColor.A = 0 32 | invisibleButtonStyle.BorderColor.A = 0 33 | invisibleButtonStyle.FocusedBorderColor.A = 0 34 | invisibleButtonStyle.DisabledBorderColor.A = 0 35 | invisibleButtonStyle.DisabledTextColor.A = 0 36 | 37 | outlineButtonStyle = ui.DefaultButtonStyle() 38 | outlineButtonStyle.BorderWidth = 4 39 | outlineButtonStyle.BackgroundColor.A = 0 40 | outlineButtonStyle.FocusedBackgroundColor.A = 0 41 | outlineButtonStyle.DisabledBackgroundColor.A = 0 42 | outlineButtonStyle.BorderColor.A = 0 43 | outlineButtonStyle.FocusedBorderColor.SetRGBA(0x25, 0x25, 0x40, 200) 44 | outlineButtonStyle.DisabledBorderColor.A = 0 45 | outlineButtonStyle.DisabledTextColor.A = 0 46 | 47 | labelButtonStyle = ui.DefaultButtonStyle() 48 | labelButtonStyle.Font = FontHandwritten 49 | labelButtonStyle.TextColor.SetRGBA(30, 30, 60, 200) 50 | labelButtonStyle.FocusedTextColor.SetRGBA(60, 60, 120, 255) 51 | labelButtonStyle.BorderWidth = 0 52 | labelButtonStyle.BackgroundColor.A = 0 53 | labelButtonStyle.FocusedBackgroundColor.A = 0 54 | labelButtonStyle.DisabledBackgroundColor.A = 0 55 | labelButtonStyle.BorderColor.A = 0 56 | labelButtonStyle.FocusedBorderColor.A = 0 57 | labelButtonStyle.DisabledBorderColor.A = 0 58 | labelButtonStyle.DisabledTextColor.A = 0 59 | } 60 | -------------------------------------------------------------------------------- /src/results_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/ge/xslices" 9 | "github.com/quasilyte/gmath" 10 | ) 11 | 12 | type resultsController struct { 13 | gameState *gameState 14 | scene *ge.Scene 15 | newManualPage string 16 | } 17 | 18 | func newResultsController(s *gameState) *resultsController { 19 | return &resultsController{gameState: s} 20 | } 21 | 22 | func (c *resultsController) Init(scene *ge.Scene) { 23 | c.scene = scene 24 | 25 | ctx := scene.Context() 26 | rect := ge.NewRect(ctx, ctx.WindowWidth, ctx.WindowWidth) 27 | rect.Centered = false 28 | rect.FillColorScale.SetRGBA(0x14, 0x18, 0x13, 0xff) 29 | scene.AddGraphics(rect) 30 | 31 | var textLines []string 32 | addList := func(title string, lines []string) { 33 | if len(lines) == 0 { 34 | return 35 | } 36 | textLines = append(textLines, "\n"+title+":\n") 37 | for _, l := range lines { 38 | textLines = append(textLines, " * "+l) 39 | } 40 | } 41 | 42 | percent := gmath.Percentage(len(c.gameState.data.CompletedLevels), len(theStoryModeMap.levels)) 43 | textLines = append(textLines, fmt.Sprintf("success! hexagon is now %d%% hacked", percent)) 44 | 45 | newContent := calculateContentStatus(c.gameState) 46 | 47 | newFeatures := xslices.Diff(c.gameState.content.techLevelFeatures, newContent.techLevelFeatures) 48 | if len(newFeatures) > 1 { 49 | panic("more than one feature is unlocked in on level?") 50 | } 51 | if len(newFeatures) != 0 { 52 | textLines = append(textLines, "\n> unlocked the "+newFeatures[0]+" feature") 53 | } 54 | 55 | newManualPages := xslices.Diff(c.gameState.content.manualPages, newContent.manualPages) 56 | addList("> new notes", newManualPages) 57 | 58 | if !xslices.Equal(c.gameState.content.chapters, newContent.chapters) { 59 | textLines = append(textLines, "\n> new blocks are accessible") 60 | } 61 | textLines = append(textLines, "\npress 'enter' to continue") 62 | if len(newManualPages) != 0 { 63 | c.newManualPage = newManualPages[0] 64 | textLines = append(textLines, "\npress 'ctrl+enter' to see new manual pages") 65 | } 66 | 67 | l := scene.NewLabel(FontLCDNormal) 68 | l.ColorScale.SetColor(defaultLCDColor) 69 | l.Pos.Offset = gmath.Vec{X: 64, Y: 64} 70 | l.Text = strings.Join(textLines, "\n") 71 | scene.AddGraphics(l) 72 | } 73 | 74 | func (c *resultsController) Update(delta float64) { 75 | if c.gameState.input.ActionIsJustPressed(ActionMenuConfirm) { 76 | c.scene.Audio().PauseCurrentMusic() 77 | c.scene.Context().ChangeScene(newLevelSelectController(c.gameState)) 78 | } 79 | if c.newManualPage != "" && c.gameState.input.ActionIsJustPressed(ActionInstantRunProgram) { 80 | c.scene.Audio().PauseCurrentMusic() 81 | c.scene.Context().ChangeScene(newManualControler(c.gameState, c.newManualPage)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/content_management.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type contentStatus struct { 4 | techLevelFeatures []string 5 | chapters []string 6 | manualPages []string 7 | levelsCompleted int 8 | bonusLevelsCompleted int 9 | solvedShift bool 10 | solvedIncDec bool 11 | solvedAtbash bool 12 | solvedPolygraphic bool 13 | solvedRot13 bool 14 | solvedNegation bool 15 | solvedCondTransform bool 16 | hackedEverything bool 17 | usedCheats bool 18 | usedHiddenKeybinds bool 19 | sawCollision bool 20 | } 21 | 22 | func calculateContentStatus(state *gameState) contentStatus { 23 | result := contentStatus{ 24 | solvedPolygraphic: state.data.SolvedPolygraphic, 25 | solvedShift: state.data.SolvedShift, 26 | solvedIncDec: state.data.SolvedIncDec, 27 | solvedAtbash: state.data.SolvedAtbash, 28 | solvedRot13: state.data.SolvedRot13, 29 | solvedNegation: state.data.SolvedNegation, 30 | solvedCondTransform: state.data.SolvedCondTransform, 31 | usedCheats: state.data.UsedCheats, 32 | usedHiddenKeybinds: state.data.UsedHiddenKeybinds, 33 | sawCollision: state.data.SawCollision, 34 | } 35 | 36 | chaptersCleared := 0 37 | for i := range theStoryModeMap.chapters { 38 | chapter := &theStoryModeMap.chapters[i] 39 | completionData := state.GetChapterCompletionData(chapter) 40 | if completionData.allLevelsCompleted && !chapter.IsBonus() { 41 | chaptersCleared++ 42 | } 43 | for _, levelName := range chapter.levels { 44 | levelCompletionData := state.GetLevelCompletionData(levelName) 45 | if levelCompletionData != nil { 46 | if chapter.IsBonus() { 47 | result.bonusLevelsCompleted++ 48 | } else { 49 | result.levelsCompleted++ 50 | } 51 | } 52 | } 53 | available := true 54 | if chapter.requires != "" { 55 | otherChapter := theStoryModeMap.getChapter(chapter.requires) 56 | otherChapterStatus := state.GetChapterCompletionData(otherChapter) 57 | if chapter.IsBonus() { 58 | available = otherChapterStatus.secretDecoded 59 | } else { 60 | available = otherChapterStatus.partiallyCompleted 61 | } 62 | } 63 | if available { 64 | result.chapters = append(result.chapters, chapter.name) 65 | } 66 | } 67 | result.hackedEverything = result.levelsCompleted+result.bonusLevelsCompleted == len(theStoryModeMap.levels) 68 | 69 | techLevel := chaptersCleared 70 | result.techLevelFeatures = append(result.techLevelFeatures, "value inspector") 71 | if techLevel >= 1 { 72 | result.techLevelFeatures = append(result.techLevelFeatures, "text buffer") 73 | } 74 | if techLevel >= 2 { 75 | result.techLevelFeatures = append(result.techLevelFeatures, "branching info") 76 | } 77 | if techLevel >= 3 { 78 | result.techLevelFeatures = append(result.techLevelFeatures, "i/o logs") 79 | } 80 | if techLevel >= 4 { 81 | result.techLevelFeatures = append(result.techLevelFeatures, "output predictor") 82 | } 83 | if techLevel >= 5 { 84 | result.techLevelFeatures = append(result.techLevelFeatures, "advanced input") 85 | } 86 | 87 | for _, p := range theGameManual.pages { 88 | if p.cond(&result) { 89 | result.manualPages = append(result.manualPages, p.title) 90 | } 91 | } 92 | 93 | return result 94 | } 95 | -------------------------------------------------------------------------------- /src/text_ops.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func checkAnagram(s1, s2 []byte) bool { 4 | if len(s1) != len(s2) { 5 | return false 6 | } 7 | var chars1 [256]byte 8 | var chars2 [256]byte 9 | for _, ch := range s1 { 10 | chars1[ch]++ 11 | } 12 | for _, ch := range s2 { 13 | chars2[ch]++ 14 | } 15 | for _, ch := range s1 { 16 | if chars1[ch] != chars2[ch] { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | func rotateCharsRight(chars []byte) { 24 | if len(chars) == 0 { 25 | return 26 | } 27 | last := chars[len(chars)-1] 28 | for i := len(chars) - 1; i >= 1; i-- { 29 | chars[i] = chars[i-1] 30 | } 31 | chars[0] = last 32 | } 33 | 34 | func rotateCharsLeft(chars []byte) { 35 | if len(chars) == 0 { 36 | return 37 | } 38 | first := chars[0] 39 | for i := 0; i < len(chars)-1; i++ { 40 | chars[i] = chars[i+1] 41 | } 42 | chars[len(chars)-1] = first 43 | } 44 | 45 | func mapChars(chars []byte, f func(ch byte) byte) { 46 | for i, ch := range chars { 47 | chars[i] = f(ch) 48 | } 49 | } 50 | 51 | func mapEvenChars(chars []byte, f func(ch byte) byte) { 52 | for i, ch := range chars { 53 | pos := i + 1 54 | if pos%2 == 0 { 55 | chars[i] = f(ch) 56 | } 57 | } 58 | } 59 | 60 | func mapOddChars(chars []byte, f func(ch byte) byte) { 61 | for i, ch := range chars { 62 | pos := i + 1 63 | if pos%2 != 0 { 64 | chars[i] = f(ch) 65 | } 66 | } 67 | } 68 | 69 | func mapCharsButfirst(chars []byte, f func(ch byte) byte) { 70 | if len(chars) < 2 { 71 | return 72 | } 73 | data := chars[1:] 74 | for i, ch := range data { 75 | data[i] = f(ch) 76 | } 77 | } 78 | 79 | func mapCharsButlast(chars []byte, f func(ch byte) byte) { 80 | if len(chars) < 2 { 81 | return 82 | } 83 | data := chars[:len(chars)-1] 84 | for i, ch := range data { 85 | data[i] = f(ch) 86 | } 87 | } 88 | 89 | var dottedPairs = [256]byte{ 90 | 'a': 'z', 91 | 'c': 'x', 92 | 'e': 'v', 93 | 'g': 't', 94 | 'i': 'r', 95 | 'k': 'p', 96 | } 97 | 98 | func polygraphicAtbash(chars []byte) { 99 | for i := 0; i < len(chars)-1; i++ { 100 | expectedNext := dottedPairs[chars[i]] 101 | if expectedNext == 0 { 102 | continue 103 | } 104 | if expectedNext == chars[i+1] { 105 | chars[i] = incChar(chars[i]) 106 | chars[i+1] = decChar(chars[i+1]) 107 | i++ 108 | } 109 | } 110 | } 111 | 112 | var dottedChars = [256]bool{ 113 | 'a': true, 114 | 'c': true, 115 | 'e': true, 116 | 'g': true, 117 | 'i': true, 118 | 'k': true, 119 | 'p': true, 120 | 'r': true, 121 | 't': true, 122 | 'v': true, 123 | 'x': true, 124 | 'z': true, 125 | } 126 | 127 | func incCharDotted(b byte) byte { 128 | if !dottedChars[b] { 129 | return b 130 | } 131 | return incChar(b) 132 | } 133 | 134 | func decCharUndotted(b byte) byte { 135 | if dottedChars[b] { 136 | return b 137 | } 138 | return decChar(b) 139 | } 140 | 141 | func incChar(b byte) byte { 142 | if b+1 > 'z' { 143 | return 'a' 144 | } 145 | return b + 1 146 | } 147 | 148 | func decChar(b byte) byte { 149 | if b-1 < 'a' { 150 | return 'z' 151 | } 152 | return b - 1 153 | } 154 | 155 | func (r *schemaRunner) decCharNowrap(b byte) byte { 156 | if b-1 < 'a' { 157 | return 'a' 158 | } 159 | return b - 1 160 | } 161 | 162 | func (r *schemaRunner) incCharNowrap(b byte) byte { 163 | if b+1 > 'z' { 164 | return 'z' 165 | } 166 | return b + 1 167 | } 168 | -------------------------------------------------------------------------------- /src/terminal_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/gmath" 9 | ) 10 | 11 | type terminalNode struct { 12 | config terminalConfig 13 | 14 | text *ge.Label 15 | 16 | offset gmath.Vec 17 | 18 | textBuffer string 19 | } 20 | 21 | type statusInfo struct { 22 | value string 23 | ioLogs []string 24 | predictedOutput string 25 | } 26 | 27 | type terminalConfig struct { 28 | username string 29 | 30 | branchHints []string 31 | 32 | upgrades terminalUpgrades 33 | } 34 | 35 | type terminalUpgrades struct { 36 | ioLog bool 37 | branchingInfo bool 38 | textBuffer bool 39 | valueInspector bool 40 | outputPredictor bool 41 | } 42 | 43 | func newTerminalNode(config terminalConfig) *terminalNode { 44 | return &terminalNode{ 45 | config: config, 46 | offset: gmath.Vec{X: 96, Y: 96}, 47 | } 48 | } 49 | 50 | func (n *terminalNode) Init(scene *ge.Scene) { 51 | n.text = scene.NewLabel(FontLCDSmall) 52 | n.text.Pos.Offset = n.offset.Add(gmath.Vec{X: 16, Y: 16}) 53 | n.text.ColorScale.SetColor(defaultLCDColor) 54 | n.text.Visible = false 55 | scene.AddGraphics(n.text) 56 | } 57 | 58 | func (n *terminalNode) SetVisible(visible bool) { 59 | n.text.Visible = visible 60 | } 61 | 62 | func (n *terminalNode) IsDisposed() bool { return false } 63 | 64 | func (n *terminalNode) Update(delta float64) {} 65 | 66 | func (n *terminalNode) GetBufferText() (string, bool) { 67 | if n.config.upgrades.textBuffer { 68 | return n.textBuffer, true 69 | } 70 | return "", false 71 | } 72 | 73 | func (n *terminalNode) UpdateInfo(info statusInfo) { 74 | greeting := n.config.username + "@decodeos $ statusdump" 75 | 76 | textlines := []string{ 77 | greeting, 78 | } 79 | 80 | if n.config.upgrades.valueInspector { 81 | textlines = append(textlines, "", fmt.Sprintf("current value: %s", info.value)) 82 | } else { 83 | textlines = append(textlines, "", "current value: unavailable") 84 | } 85 | 86 | if n.config.upgrades.branchingInfo { 87 | if len(n.config.branchHints) == 0 { 88 | textlines = append(textlines, "", "branching info: no branches") 89 | } else { 90 | textlines = append(textlines, "") 91 | for i, b := range n.config.branchHints { 92 | if i == 0 { 93 | textlines = append(textlines, "branching info: * "+b) 94 | } else { 95 | textlines = append(textlines, " * "+b) 96 | } 97 | } 98 | } 99 | } else { 100 | textlines = append(textlines, "", "branching info: unavailable") 101 | } 102 | 103 | if n.config.upgrades.ioLog { 104 | if len(info.ioLogs) == 0 { 105 | textlines = append(textlines, "", "i/o logs: no data") 106 | } else { 107 | textlines = append(textlines, "") 108 | for i, l := range info.ioLogs { 109 | if i == 0 { 110 | textlines = append(textlines, "i/o logs: * "+l) 111 | } else { 112 | textlines = append(textlines, " * "+l) 113 | } 114 | } 115 | } 116 | } else { 117 | textlines = append(textlines, "", "i/o logs: unavailable") 118 | } 119 | 120 | if n.config.upgrades.textBuffer { 121 | textBuffer := n.textBuffer 122 | if textBuffer == "" { 123 | textBuffer = "" 124 | } 125 | textlines = append(textlines, "", "text buffer: "+textBuffer) 126 | } else { 127 | textlines = append(textlines, "", "text buffer: unavailable") 128 | } 129 | 130 | if n.config.upgrades.outputPredictor { 131 | textlines = append(textlines, "", "output prediction: "+info.predictedOutput) 132 | } else { 133 | textlines = append(textlines, "", "output prediction: unavailable") 134 | } 135 | 136 | n.text.Text = strings.Join(textlines, "\n") 137 | } 138 | -------------------------------------------------------------------------------- /src/leveldata/schema.go: -------------------------------------------------------------------------------- 1 | package leveldata 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | 7 | "github.com/quasilyte/gmath" 8 | ) 9 | 10 | type ComponentSchema struct { 11 | Entry *SchemaElem 12 | 13 | Elems []*SchemaElem 14 | 15 | NumKeywords int 16 | Keywords []string 17 | EncodedKeywords []string 18 | 19 | HasCondTransform bool 20 | HasPolygraphic bool 21 | HasAtbash bool 22 | HasRot13 bool 23 | HasIncDec bool 24 | HasShift bool 25 | HasNegation bool 26 | } 27 | 28 | type SchemaElemKind int 29 | 30 | const ( 31 | UnknownElem SchemaElemKind = iota 32 | InputElem 33 | OutputElem 34 | MuxElem 35 | SimplePipeElem 36 | PipeConnect2Elem 37 | IfElem 38 | TransformElem 39 | ) 40 | 41 | type SchemaElem struct { 42 | ElemID int 43 | TileClassID int 44 | TileClass string 45 | Kind SchemaElemKind 46 | 47 | Next []*SchemaElem 48 | 49 | Pos gmath.Vec 50 | 51 | Rotation gmath.Rad 52 | 53 | ExtraData any 54 | } 55 | 56 | func getElemShape(class string, pos gmath.Vec, rotation gmath.Rad, extraData any) elemShape { 57 | var shape elemShape 58 | switch class { 59 | case "pipe_connect2": 60 | // Two inputs, one output. 61 | // The default orientation is: 62 | // 63 | // v 64 | // | 65 | // .--> 66 | // | 67 | // ^ 68 | // 69 | startRotation1 := rotation + (math.Pi / 2) 70 | startRotation2 := rotation - (math.Pi / 2) 71 | endRotation := rotation 72 | shape.inputs[0] = pos.MoveInDirection(48, startRotation1-math.Pi) 73 | shape.inputs[1] = pos.MoveInDirection(48, startRotation2-math.Pi) 74 | shape.numInputs = 2 75 | shape.outputs[0] = pos.MoveInDirection(48, endRotation) 76 | shape.numOutputs = 1 77 | 78 | case "angle_pipe", "special_angle_pipe": 79 | // One input, out output. Can be flipped. 80 | // The default orientation is: 81 | // 82 | // >--. 83 | // | 84 | // v 85 | // 86 | startRotation := rotation 87 | endRotation := rotation + (math.Pi / 2) 88 | extra := extraData.(*AngleElemExtra) 89 | if extra.FlipHorizontally { 90 | startRotation += math.Pi 91 | } 92 | shape.inputs[0] = pos.MoveInDirection(48, startRotation-math.Pi) 93 | shape.numInputs = 1 94 | shape.outputs[0] = pos.MoveInDirection(48, endRotation) 95 | shape.numOutputs = 1 96 | 97 | case "pipe", "special_pipe": 98 | // One input, out output. 99 | shape.inputs[0] = pos.MoveInDirection(48, rotation-math.Pi) 100 | shape.numInputs = 1 101 | shape.outputs[0] = pos.MoveInDirection(48, rotation) 102 | shape.numOutputs = 1 103 | 104 | default: 105 | // The default shape allows any kind of connection from any direction. 106 | left := gmath.Vec{X: -48} 107 | right := gmath.Vec{X: 48} 108 | up := gmath.Vec{Y: -48} 109 | down := gmath.Vec{Y: 48} 110 | shape.inputs = [4]gmath.Vec{left, right, up, down} 111 | shape.numInputs = 4 112 | shape.outputs = [4]gmath.Vec{left, right, up, down} 113 | shape.numOutputs = 4 114 | for i := 0; i < shape.numInputs; i++ { 115 | shape.inputs[i] = shape.inputs[i].Add(pos) 116 | } 117 | for i := 0; i < shape.numOutputs; i++ { 118 | shape.outputs[i] = shape.outputs[i].Add(pos) 119 | } 120 | } 121 | 122 | return shape 123 | } 124 | 125 | func getSchemaElemKind(tileClass string) SchemaElemKind { 126 | switch tileClass { 127 | case "elem_input": 128 | return InputElem 129 | case "elem_output": 130 | return OutputElem 131 | case "elem_mux": 132 | return MuxElem 133 | case "elem_if", "elem_ifnot", "elem_repeater", "elem_inv_repeater": 134 | return IfElem 135 | case "pipe_connect2": 136 | return PipeConnect2Elem 137 | } 138 | 139 | if strings.HasPrefix(tileClass, "apply_") { 140 | return TransformElem 141 | } 142 | if strings.Contains(tileClass, "_countdown") { 143 | return IfElem 144 | } 145 | if strings.Contains(tileClass, "pipe") { 146 | return SimplePipeElem 147 | } 148 | 149 | return UnknownElem 150 | } 151 | -------------------------------------------------------------------------------- /src/main_menu_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/ge/ui" 9 | "github.com/quasilyte/gmath" 10 | ) 11 | 12 | type mainMenuController struct { 13 | gameState *gameState 14 | scene *ge.Scene 15 | } 16 | 17 | func newMainMenuController(s *gameState) *mainMenuController { 18 | return &mainMenuController{gameState: s} 19 | } 20 | 21 | func (c *mainMenuController) Init(scene *ge.Scene) { 22 | c.scene = scene 23 | 24 | scene.Audio().SetGroupVolume(SoundGroupMusic, volumeMultiplier(c.gameState.data.Options.MusicVolumeLevel)) 25 | scene.Audio().SetGroupVolume(SoundGroupEffect, volumeMultiplier(c.gameState.data.Options.EffectsVolumeLevel)) 26 | if c.gameState.data.Options.MusicVolumeLevel != 0 { 27 | scene.Audio().ContinueMusic(AudioMenuMusic) 28 | } 29 | 30 | bg := scene.NewSprite(ImagePaperBg) 31 | bg.Centered = false 32 | scene.AddGraphics(bg) 33 | 34 | layer := ge.NewShaderLayer() 35 | layer.Shader = scene.NewShader(ShaderHandwriting) 36 | 37 | offset := gmath.Vec{X: 512, Y: 76} 38 | l := scene.NewLabel(FontHandwritten) 39 | l.Text = `What should I do? 40 | 41 | - Get back to work 42 | 43 | - Review the notes 44 | 45 | - Adjust the options` 46 | if runtime.GOARCH != "wasm" { 47 | l.Text += "\n\n - Run a custom simulation" 48 | l.Text += "\n\n - Call it a day" 49 | } 50 | l.Pos.Offset = offset 51 | l.ColorScale.SetRGBA(30, 30, 60, 220) 52 | layer.AddGraphics(l) 53 | 54 | c.initUI(offset) 55 | 56 | scene.AddGraphics(layer) 57 | } 58 | 59 | func (c *mainMenuController) initUI(offset gmath.Vec) { 60 | uiRoot := ui.NewRoot(c.scene.Context(), c.gameState.input) 61 | uiRoot.ActivationAction = ActionMenuConfirm 62 | uiRoot.NextInputAction = ActionMenuNext 63 | uiRoot.PrevInputAction = ActionMenuPrev 64 | c.scene.AddObject(uiRoot) 65 | 66 | var bgroup buttonGroup 67 | 68 | offset = offset.Add(gmath.Vec{X: 96, Y: 156}) 69 | 70 | storyModeButton := uiRoot.NewButton(outlineButtonStyle.Resized(648, 80)) 71 | bgroup.AddButton(storyModeButton) 72 | storyModeButton.Pos.Offset = offset 73 | storyModeButton.EventActivated.Connect(nil, func(_ *ui.Button) { 74 | c.scene.Context().ChangeScene(newChapterSelectController(c.gameState)) 75 | }) 76 | c.scene.AddObject(storyModeButton) 77 | 78 | offset.Y += 166 79 | 80 | reviewNotesButton := uiRoot.NewButton(outlineButtonStyle.Resized(632, 80)) 81 | bgroup.AddButton(reviewNotesButton) 82 | reviewNotesButton.Pos.Offset = offset 83 | reviewNotesButton.EventActivated.Connect(nil, func(_ *ui.Button) { 84 | c.scene.Context().ChangeScene(newManualControler(c.gameState, "")) 85 | }) 86 | c.scene.AddObject(reviewNotesButton) 87 | 88 | offset.Y += 166 89 | 90 | optionsButton := uiRoot.NewButton(outlineButtonStyle.Resized(668, 80)) 91 | bgroup.AddButton(optionsButton) 92 | optionsButton.Pos.Offset = offset 93 | optionsButton.EventActivated.Connect(nil, func(_ *ui.Button) { 94 | c.scene.Context().ChangeScene(newOptionsController(c.gameState)) 95 | }) 96 | c.scene.AddObject(optionsButton) 97 | 98 | offset.Y += 166 99 | 100 | if runtime.GOARCH != "wasm" { 101 | customModeButton := uiRoot.NewButton(outlineButtonStyle.Resized(848, 80)) 102 | bgroup.AddButton(customModeButton) 103 | customModeButton.Pos.Offset = offset 104 | customModeButton.EventActivated.Connect(nil, func(_ *ui.Button) { 105 | c.scene.Context().ChangeScene(newCustomLevelSelectController(c.gameState)) 106 | }) 107 | c.scene.AddObject(customModeButton) 108 | offset.Y += 166 109 | 110 | exitButton := uiRoot.NewButton(outlineButtonStyle.Resized(460, 80)) 111 | bgroup.AddButton(exitButton) 112 | exitButton.Pos.Offset = offset 113 | exitButton.EventActivated.Connect(nil, func(_ *ui.Button) { 114 | os.Exit(0) 115 | }) 116 | c.scene.AddObject(exitButton) 117 | } 118 | 119 | bgroup.Connect(uiRoot) 120 | bgroup.FocusFirst() 121 | } 122 | 123 | func (c *mainMenuController) Update(delta float64) { 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/manual_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/ge/xslices" 9 | "github.com/quasilyte/gmath" 10 | ) 11 | 12 | type manualController struct { 13 | scene *ge.Scene 14 | gameState *gameState 15 | 16 | pageSlider gmath.Slider 17 | pagesAvailable []gameManualPage 18 | 19 | bg *ge.Sprite 20 | illustration *ge.Sprite 21 | text *ge.Label 22 | 23 | open string 24 | } 25 | 26 | func newManualControler(s *gameState, open string) *manualController { 27 | return &manualController{ 28 | gameState: s, 29 | open: open, 30 | } 31 | } 32 | 33 | func (c *manualController) Init(scene *ge.Scene) { 34 | c.scene = scene 35 | 36 | if c.gameState.data.Options.MusicVolumeLevel != 0 { 37 | scene.Audio().ContinueMusic(AudioMenuMusic) 38 | } 39 | 40 | content := calculateContentStatus(c.gameState) 41 | for _, p := range theGameManual.pages { 42 | if xslices.Contains(content.manualPages, p.title) { 43 | c.pagesAvailable = append(c.pagesAvailable, p) 44 | } 45 | } 46 | c.pageSlider.SetBounds(0, len(c.pagesAvailable)-1) 47 | 48 | c.bg = scene.NewSprite(ImagePaperBg) 49 | c.bg.Centered = false 50 | scene.AddGraphics(c.bg) 51 | 52 | layer := ge.NewShaderLayer() 53 | layer.Shader = scene.NewShader(ShaderHandwriting) 54 | 55 | c.text = scene.NewLabel(FontHandwritten) 56 | c.text.Pos.Offset = gmath.Vec{X: 176, Y: 72} 57 | c.text.ColorScale.SetRGBA(30, 30, 60, 220) 58 | layer.AddGraphics(c.text) 59 | 60 | scene.AddGraphics(layer) 61 | 62 | c.illustration = ge.NewSprite(scene.Context()) 63 | c.illustration.Centered = false 64 | c.illustration.Visible = false 65 | c.illustration.Pos.Offset.X = 1370 66 | scene.AddGraphics(c.illustration) 67 | 68 | pageIndex := 0 69 | if c.open != "" { 70 | for i, p := range c.pagesAvailable { 71 | if p.title == c.open { 72 | pageIndex = i 73 | break 74 | } 75 | } 76 | } 77 | c.pageSlider.TrySetValue(pageIndex) 78 | c.flipPage(c.pagesAvailable[pageIndex]) 79 | } 80 | 81 | func (c *manualController) flipPage(p gameManualPage) { 82 | flipHorizontal := c.bg.FlipHorizontal 83 | flipVertical := c.bg.FlipVertical 84 | for { 85 | c.bg.FlipHorizontal = c.scene.Rand().Bool() 86 | c.bg.FlipVertical = c.scene.Rand().Bool() 87 | if c.bg.FlipHorizontal != flipHorizontal || c.bg.FlipVertical != flipVertical { 88 | break 89 | } 90 | } 91 | 92 | if p.image != ImageNone { 93 | c.illustration.SetImage(c.scene.LoadImage(p.image)) 94 | c.illustration.Visible = true 95 | } else { 96 | c.illustration.Visible = false 97 | } 98 | 99 | var buf strings.Builder 100 | if p.title != "" { 101 | buf.WriteString(p.title) 102 | buf.WriteRune('\n') 103 | buf.WriteRune('\n') 104 | } 105 | text := p.text 106 | if p.params != nil { 107 | text = fmt.Sprintf(text, p.params(c.gameState.data)...) 108 | } 109 | lines := strings.Split(text, "\n") 110 | for _, l := range lines { 111 | l = strings.TrimSpace(l) 112 | if l == "" { 113 | continue 114 | } 115 | if strings.HasPrefix(l, "> ") { 116 | l = strings.TrimPrefix(l, "> ") 117 | buf.WriteString(" ") 118 | } 119 | if l == "\\n" { 120 | buf.WriteByte('\n') 121 | continue 122 | } 123 | buf.WriteString(l) 124 | buf.WriteByte('\n') 125 | } 126 | c.text.Text = buf.String() 127 | } 128 | 129 | func (c *manualController) Update(delta float64) { 130 | if c.gameState.input.ActionIsJustPressed(ActionLeave) { 131 | c.leave() 132 | return 133 | } 134 | if c.gameState.input.ActionIsJustPressed(ActionMenuNextPage) || c.gameState.input.ActionIsJustPressed(ActionMenuConfirm) { 135 | c.nextPage() 136 | return 137 | } 138 | if c.gameState.input.ActionIsJustPressed(ActionMenuPrevPage) { 139 | c.prevPage() 140 | return 141 | } 142 | } 143 | 144 | func (c *manualController) nextPage() { 145 | c.pageSlider.Inc() 146 | c.flipPage(c.pagesAvailable[c.pageSlider.Value()]) 147 | } 148 | 149 | func (c *manualController) prevPage() { 150 | c.pageSlider.Dec() 151 | c.flipPage(c.pagesAvailable[c.pageSlider.Value()]) 152 | } 153 | 154 | func (c *manualController) leave() { 155 | c.scene.Context().ChangeScene(newMainMenuController(c.gameState)) 156 | } 157 | -------------------------------------------------------------------------------- /src/_assets/levels/bonus/lossy_conversion.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":384, 20 | "y":288 21 | }, 22 | { 23 | "class":"", 24 | "gid":12, 25 | "height":96, 26 | "id":115, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":672, 32 | "y":576 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"lynx\nbanana\nsoftware\nrogue\nhypno\npluto\n" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":3 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":36, 60 | "height":96, 61 | "id":117, 62 | "name":"", 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":576, 67 | "y":480 68 | }, 69 | { 70 | "class":"", 71 | "gid":37, 72 | "height":96, 73 | "id":118, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":480, 79 | "y":384 80 | }, 81 | { 82 | "class":"", 83 | "gid":8, 84 | "height":96, 85 | "id":122, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":480, 91 | "y":288 92 | }, 93 | { 94 | "class":"", 95 | "gid":8, 96 | "height":96, 97 | "id":126, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":384 104 | }, 105 | { 106 | "class":"", 107 | "gid":8, 108 | "height":96, 109 | "id":127, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":672, 115 | "y":480 116 | }], 117 | "opacity":1, 118 | "type":"objectgroup", 119 | "visible":true, 120 | "x":0, 121 | "y":0 122 | }], 123 | "nextlayerid":3, 124 | "nextobjectid":128, 125 | "orientation":"orthogonal", 126 | "renderorder":"right-down", 127 | "tiledversion":"1.9.2", 128 | "tileheight":96, 129 | "tilesets":[ 130 | { 131 | "firstgid":1, 132 | "source":"..\/..\/schemas.tsj" 133 | }], 134 | "tilewidth":96, 135 | "type":"map", 136 | "version":"1.9", 137 | "width":12 138 | } -------------------------------------------------------------------------------- /src/leveldata/template.go: -------------------------------------------------------------------------------- 1 | package leveldata 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/quasilyte/ge/tiled" 8 | "github.com/quasilyte/gmath" 9 | ) 10 | 11 | type SchemaTemplate struct { 12 | Tileset *tiled.Tileset 13 | Elems []SchemaTemplateElem 14 | NumKeywords int 15 | Keywords []string 16 | Hints []SchemaHintTemplate 17 | } 18 | 19 | type SchemaHintTemplate struct { 20 | Text string 21 | Pos gmath.Vec 22 | } 23 | 24 | type SchemaTemplateElem struct { 25 | Class string 26 | ClassID int 27 | Rotation gmath.Rad 28 | ExtraData any 29 | Pos gmath.Vec 30 | } 31 | 32 | type AngleElemExtra struct { 33 | FlipHorizontally bool 34 | } 35 | 36 | type CountdownElemExtra struct { 37 | InitialValue int 38 | } 39 | 40 | type IfElemExtra struct { 41 | CondKind string 42 | StringArg string 43 | IntArg int 44 | } 45 | 46 | func ValidateLevelData(tileset *tiled.Tileset, levelData []byte) error { 47 | tmpl, err := LoadLevelTemplate(tileset, levelData) 48 | if err != nil { 49 | return err 50 | } 51 | _, err = NewSchemaBuilder(gmath.Vec{}, tmpl).Build() 52 | return err 53 | } 54 | 55 | func LoadLevelTemplate(tileset *tiled.Tileset, levelData []byte) (*SchemaTemplate, error) { 56 | m, err := tiled.UnmarshalMap(levelData) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return TilemapToTemplate(tileset, m) 61 | } 62 | 63 | func TilemapToTemplate(tileset *tiled.Tileset, m *tiled.Map) (*SchemaTemplate, error) { 64 | calcObjectPos := func(o tiled.Object) gmath.Vec { 65 | pos := gmath.Vec{X: float64(o.X) + tileset.TileWidth/2, Y: float64(o.Y) - tileset.TileHeight/2} 66 | switch o.Rotation { 67 | case 90: 68 | pos.Y += tileset.TileHeight 69 | case 180: 70 | pos.X -= tileset.TileWidth 71 | pos.Y += tileset.TileHeight 72 | case 270: 73 | pos.X -= tileset.TileWidth 74 | } 75 | return pos 76 | } 77 | 78 | var result SchemaTemplate 79 | 80 | result.Tileset = tileset 81 | 82 | elemList := make([]SchemaTemplateElem, 0, 24) 83 | 84 | ref := m.Tilesets[0] 85 | layer := m.Layers[0] 86 | 87 | foundSettings := false 88 | for _, o := range layer.Objects { 89 | id := o.GID - ref.FirstGID 90 | t := tileset.TileByID(id) 91 | pos := calcObjectPos(o) 92 | if t.Class == "settings" { 93 | if foundSettings { 94 | return nil, fmt.Errorf("%v: found more than one settings element", pos) 95 | } 96 | foundSettings = true 97 | allKeywords := strings.TrimSpace(o.GetStringProp("keywords", "")) 98 | if allKeywords == "" { 99 | return nil, fmt.Errorf("%v: settings.keywords property is empty", pos) 100 | } 101 | keywordList := strings.Split(allKeywords, "\n") 102 | result.NumKeywords = o.GetIntProp("num_keywords", 0) 103 | result.Keywords = keywordList 104 | continue 105 | } 106 | if t.Class == "hint" { 107 | pos := gmath.Vec{X: float64(o.X), Y: float64(o.Y)} 108 | result.Hints = append(result.Hints, SchemaHintTemplate{ 109 | Text: o.GetStringProp("text", ""), 110 | Pos: pos, 111 | }) 112 | continue 113 | } 114 | elem := SchemaTemplateElem{ 115 | Pos: pos, 116 | ClassID: t.Index, 117 | Class: t.Class, 118 | Rotation: gmath.DegToRad(float64(o.Rotation)), 119 | } 120 | switch elem.Class { 121 | case "angle_pipe", "special_angle_pipe": 122 | if o.FlippedVertically() { 123 | return nil, fmt.Errorf("%v: vertical flipping is obsolete", pos) 124 | } 125 | elem.ExtraData = &AngleElemExtra{ 126 | FlipHorizontally: o.FlippedHorizontally(), 127 | } 128 | case "elem_countdown0", "elem_countdown1", "elem_countdown2", "elem_countdown3": 129 | extra := &CountdownElemExtra{ 130 | InitialValue: 3, 131 | } 132 | switch elem.Class { 133 | case "elem_countdown0": 134 | extra.InitialValue = 0 135 | case "elem_countdown1": 136 | extra.InitialValue = 1 137 | case "elem_countdown2": 138 | extra.InitialValue = 2 139 | } 140 | elem.ExtraData = extra 141 | case "elem_if", "elem_ifnot": 142 | extra := &IfElemExtra{ 143 | CondKind: o.GetStringProp("cond_kind", ""), 144 | StringArg: o.GetStringProp("string_arg", ""), 145 | IntArg: o.GetIntProp("int_arg", 0), 146 | } 147 | if extra.CondKind == "" { 148 | return nil, fmt.Errorf("%v: elem_if cond property is empty", pos) 149 | } 150 | elem.ExtraData = extra 151 | } 152 | elemList = append(elemList, elem) 153 | } 154 | 155 | result.Elems = elemList 156 | 157 | return &result, nil 158 | } 159 | -------------------------------------------------------------------------------- /src/options_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/quasilyte/ge" 7 | "github.com/quasilyte/ge/ui" 8 | "github.com/quasilyte/gmath" 9 | ) 10 | 11 | type optionsController struct { 12 | gameState *gameState 13 | scene *ge.Scene 14 | } 15 | 16 | func newOptionsController(s *gameState) *optionsController { 17 | return &optionsController{gameState: s} 18 | } 19 | 20 | func (c *optionsController) Init(scene *ge.Scene) { 21 | c.scene = scene 22 | 23 | ctx := scene.Context() 24 | rect := ge.NewRect(ctx, ctx.WindowWidth, ctx.WindowWidth) 25 | rect.Centered = false 26 | rect.FillColorScale.SetRGBA(0x14, 0x18, 0x13, 0xff) 27 | scene.AddGraphics(rect) 28 | 29 | buttonWidth := 640.0 30 | offset := gmath.Vec{X: ctx.WindowWidth/2 - buttonWidth/2, Y: 256 - 64} 31 | uiRoot := ui.NewRoot(ctx, c.gameState.input) 32 | uiRoot.ActivationAction = ActionMenuConfirm 33 | uiRoot.NextInputAction = ActionMenuNext 34 | uiRoot.PrevInputAction = ActionMenuPrev 35 | scene.AddObject(uiRoot) 36 | 37 | onoffText := func(v bool) string { 38 | if v { 39 | return "on" 40 | } 41 | return "off" 42 | } 43 | 44 | options := &c.gameState.data.Options 45 | 46 | var bgroup buttonGroup 47 | 48 | { 49 | musicToggle := uiRoot.NewButton(optionsButtonStyle.Resized(buttonWidth, 80)) 50 | bgroup.AddButton(musicToggle) 51 | var musicToggleValue gmath.Slider 52 | musicToggleValue.SetBounds(0, 5) 53 | musicToggleValue.TrySetValue(options.MusicVolumeLevel) 54 | musicToggle.Text = "music volume: " + strconv.Itoa(options.MusicVolumeLevel) 55 | musicToggle.Pos.Offset = offset 56 | musicToggle.EventActivated.Connect(nil, func(_ *ui.Button) { 57 | musicToggleValue.Inc() 58 | options.MusicVolumeLevel = musicToggleValue.Value() 59 | musicToggle.Text = "music volume: " + strconv.Itoa(options.MusicVolumeLevel) 60 | if options.MusicVolumeLevel != 0 { 61 | scene.Audio().SetGroupVolume(SoundGroupMusic, volumeMultiplier(options.MusicVolumeLevel)) 62 | scene.Audio().PauseCurrentMusic() 63 | scene.Audio().PlayMusic(AudioMenuMusic) 64 | } else { 65 | scene.Audio().PauseCurrentMusic() 66 | } 67 | }) 68 | scene.AddObject(musicToggle) 69 | } 70 | offset.Y += 128 71 | 72 | { 73 | effectToggle := uiRoot.NewButton(optionsButtonStyle.Resized(buttonWidth, 80)) 74 | bgroup.AddButton(effectToggle) 75 | var effectToggleValue gmath.Slider 76 | effectToggleValue.SetBounds(0, 5) 77 | effectToggleValue.TrySetValue(options.EffectsVolumeLevel) 78 | effectToggle.Text = "effects volume: " + strconv.Itoa(options.EffectsVolumeLevel) 79 | effectToggle.Pos.Offset = offset 80 | effectToggle.EventActivated.Connect(nil, func(_ *ui.Button) { 81 | effectToggleValue.Inc() 82 | options.EffectsVolumeLevel = effectToggleValue.Value() 83 | effectToggle.Text = "effects volume: " + strconv.Itoa(options.EffectsVolumeLevel) 84 | if options.EffectsVolumeLevel != 0 { 85 | scene.Audio().SetGroupVolume(SoundGroupEffect, volumeMultiplier(options.EffectsVolumeLevel)) 86 | scene.Audio().PlaySound(AudioSecretUnlocked) 87 | } 88 | }) 89 | scene.AddObject(effectToggle) 90 | } 91 | offset.Y += 128 92 | 93 | shaderToggle := uiRoot.NewButton(optionsButtonStyle.Resized(buttonWidth, 80)) 94 | bgroup.AddButton(shaderToggle) 95 | shaderToggle.Text = "crt shaders: " + onoffText(options.CrtShader) 96 | shaderToggle.Pos.Offset = offset 97 | shaderToggle.EventActivated.Connect(nil, func(_ *ui.Button) { 98 | options.CrtShader = !options.CrtShader 99 | shaderToggle.Text = "crt shaders: " + onoffText(options.CrtShader) 100 | }) 101 | scene.AddObject(shaderToggle) 102 | offset.Y += 128 103 | 104 | backButton := uiRoot.NewButton(optionsButtonStyle.Resized(buttonWidth, 80)) 105 | bgroup.AddButton(backButton) 106 | backButton.Text = "back" 107 | backButton.Pos.Offset = offset 108 | backButton.EventActivated.Connect(nil, func(_ *ui.Button) { 109 | c.leave() 110 | }) 111 | scene.AddObject(backButton) 112 | offset.Y += 128 113 | 114 | bgroup.Connect(uiRoot) 115 | bgroup.FocusFirst() 116 | 117 | version := scene.NewLabel(FontLCDSmall) 118 | version.ColorScale.SetColor(defaultLCDColor) 119 | version.Text = "build " + buildVersion 120 | version.Pos.Offset = offset 121 | version.Width = buttonWidth 122 | version.Height = 80 123 | version.AlignHorizontal = ge.AlignHorizontalCenter 124 | version.AlignVertical = ge.AlignVerticalCenter 125 | scene.AddGraphics(version) 126 | } 127 | 128 | func (c *optionsController) leave() { 129 | c.scene.Context().SaveGameData("save", *c.gameState.data) 130 | c.scene.Context().ChangeScene(newMainMenuController(c.gameState)) 131 | } 132 | 133 | func (c *optionsController) Update(delta float64) { 134 | if c.gameState.input.ActionIsJustPressed(ActionLeave) { 135 | c.leave() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/_assets/levels/story/hello_world.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":288, 20 | "y":384 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":384, 32 | "y":384 33 | }, 34 | { 35 | "class":"", 36 | "gid":4, 37 | "height":96, 38 | "id":113, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":480, 44 | "y":384 45 | }, 46 | { 47 | "class":"", 48 | "gid":5, 49 | "height":96, 50 | "id":114, 51 | "name":"", 52 | "rotation":0, 53 | "visible":true, 54 | "width":96, 55 | "x":576, 56 | "y":384 57 | }, 58 | { 59 | "class":"", 60 | "gid":12, 61 | "height":96, 62 | "id":115, 63 | "name":"", 64 | "rotation":0, 65 | "visible":true, 66 | "width":96, 67 | "x":672, 68 | "y":384 69 | }, 70 | { 71 | "class":"", 72 | "gid":21, 73 | "height":96, 74 | "id":116, 75 | "name":"", 76 | "properties":[ 77 | { 78 | "name":"keywords", 79 | "type":"string", 80 | "value":"gargoyle\nphoenix\nsphynx\ncentaur\nwerewolf\ngolem\nsatyr\nmedusa" 81 | }, 82 | { 83 | "name":"num_keywords", 84 | "type":"int", 85 | "value":2 86 | }], 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":0, 91 | "y":768 92 | }, 93 | { 94 | "class":"", 95 | "gid":57, 96 | "height":96, 97 | "id":119, 98 | "name":"", 99 | "properties":[ 100 | { 101 | "name":"text", 102 | "type":"string", 103 | "value":"[enter] runs a\nprogram" 104 | }], 105 | "rotation":0, 106 | "visible":true, 107 | "width":96, 108 | "x":864, 109 | "y":672 110 | }, 111 | { 112 | "class":"", 113 | "gid":57, 114 | "height":96, 115 | "id":120, 116 | "name":"", 117 | "properties":[ 118 | { 119 | "name":"text", 120 | "type":"string", 121 | "value":"INPUT is controlled\nby my keyboard" 122 | }], 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":480, 127 | "y":672 128 | }], 129 | "opacity":1, 130 | "type":"objectgroup", 131 | "visible":true, 132 | "x":0, 133 | "y":0 134 | }], 135 | "nextlayerid":3, 136 | "nextobjectid":121, 137 | "orientation":"orthogonal", 138 | "renderorder":"right-down", 139 | "tiledversion":"1.9.2", 140 | "tileheight":96, 141 | "tilesets":[ 142 | { 143 | "firstgid":1, 144 | "source":"..\/..\/schemas.tsj" 145 | }], 146 | "tilewidth":96, 147 | "type":"map", 148 | "version":"1.9", 149 | "width":12 150 | } -------------------------------------------------------------------------------- /src/_assets/levels/bonus/double_zigzag.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":192, 20 | "y":384 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":288, 32 | "y":384 33 | }, 34 | { 35 | "class":"", 36 | "gid":5, 37 | "height":96, 38 | "id":114, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":672, 44 | "y":384 45 | }, 46 | { 47 | "class":"", 48 | "gid":12, 49 | "height":96, 50 | "id":115, 51 | "name":"", 52 | "rotation":0, 53 | "visible":true, 54 | "width":96, 55 | "x":768, 56 | "y":384 57 | }, 58 | { 59 | "class":"", 60 | "gid":21, 61 | "height":96, 62 | "id":116, 63 | "name":"", 64 | "properties":[ 65 | { 66 | "name":"keywords", 67 | "type":"string", 68 | "value":"sphere\nstrife\nphantom\ndelusion\ndion\ngludio" 69 | }, 70 | { 71 | "name":"num_keywords", 72 | "type":"int", 73 | "value":3 74 | }], 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":0, 79 | "y":768 80 | }, 81 | { 82 | "class":"", 83 | "gid":50, 84 | "height":96, 85 | "id":118, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":384, 91 | "y":384 92 | }, 93 | { 94 | "class":"", 95 | "gid":46, 96 | "height":96, 97 | "id":119, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":480, 103 | "y":288 104 | }, 105 | { 106 | "class":"", 107 | "gid":50, 108 | "height":96, 109 | "id":120, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":576, 115 | "y":384 116 | }, 117 | { 118 | "class":"", 119 | "gid":8, 120 | "height":96, 121 | "id":121, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":576, 127 | "y":288 128 | }, 129 | { 130 | "class":"", 131 | "gid":8, 132 | "height":96, 133 | "id":122, 134 | "name":"", 135 | "rotation":270, 136 | "visible":true, 137 | "width":96, 138 | "x":480, 139 | "y":288 140 | }], 141 | "opacity":1, 142 | "type":"objectgroup", 143 | "visible":true, 144 | "x":0, 145 | "y":0 146 | }], 147 | "nextlayerid":3, 148 | "nextobjectid":123, 149 | "orientation":"orthogonal", 150 | "renderorder":"right-down", 151 | "tiledversion":"1.9.2", 152 | "tileheight":96, 153 | "tilesets":[ 154 | { 155 | "firstgid":1, 156 | "source":"..\/..\/schemas.tsj" 157 | }], 158 | "tilewidth":96, 159 | "type":"map", 160 | "version":"1.9", 161 | "width":12 162 | } -------------------------------------------------------------------------------- /src/_assets/levels/story/swap_shifter.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":288, 20 | "y":384 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":384, 32 | "y":384 33 | }, 34 | { 35 | "class":"", 36 | "gid":5, 37 | "height":96, 38 | "id":114, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":576, 44 | "y":384 45 | }, 46 | { 47 | "class":"", 48 | "gid":12, 49 | "height":96, 50 | "id":115, 51 | "name":"", 52 | "rotation":0, 53 | "visible":true, 54 | "width":96, 55 | "x":576, 56 | "y":288 57 | }, 58 | { 59 | "class":"", 60 | "gid":21, 61 | "height":96, 62 | "id":116, 63 | "name":"", 64 | "properties":[ 65 | { 66 | "name":"keywords", 67 | "type":"string", 68 | "value":"terran\ndusk\ntemplate\noccupant\ntriton\nhyperspace" 69 | }, 70 | { 71 | "name":"num_keywords", 72 | "type":"int", 73 | "value":2 74 | }], 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":0, 79 | "y":768 80 | }, 81 | { 82 | "class":"", 83 | "gid":39, 84 | "height":96, 85 | "id":117, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":480, 91 | "y":384 92 | }, 93 | { 94 | "class":"", 95 | "gid":7, 96 | "height":96, 97 | "id":119, 98 | "name":"", 99 | "rotation":90, 100 | "visible":true, 101 | "width":96, 102 | "x":672, 103 | "y":384 104 | }, 105 | { 106 | "class":"", 107 | "gid":43, 108 | "height":96, 109 | "id":120, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":576, 115 | "y":480 116 | }, 117 | { 118 | "class":"", 119 | "gid":8, 120 | "height":96, 121 | "id":121, 122 | "name":"", 123 | "rotation":180, 124 | "visible":true, 125 | "width":96, 126 | "x":576, 127 | "y":384 128 | }, 129 | { 130 | "class":"", 131 | "gid":2147483656, 132 | "height":96, 133 | "id":122, 134 | "name":"", 135 | "rotation":90, 136 | "visible":true, 137 | "width":96, 138 | "x":672, 139 | "y":192 140 | }, 141 | { 142 | "class":"", 143 | "gid":34, 144 | "height":96, 145 | "id":123, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":672, 151 | "y":384 152 | }], 153 | "opacity":1, 154 | "type":"objectgroup", 155 | "visible":true, 156 | "x":0, 157 | "y":0 158 | }], 159 | "nextlayerid":3, 160 | "nextobjectid":124, 161 | "orientation":"orthogonal", 162 | "renderorder":"right-down", 163 | "tiledversion":"1.9.2", 164 | "tileheight":96, 165 | "tilesets":[ 166 | { 167 | "firstgid":1, 168 | "source":"..\/..\/schemas.tsj" 169 | }], 170 | "tilewidth":96, 171 | "type":"map", 172 | "version":"1.9", 173 | "width":12 174 | } -------------------------------------------------------------------------------- /src/component_input.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/quasilyte/ge" 8 | "github.com/quasilyte/ge/gesignal" 9 | "github.com/quasilyte/ge/input" 10 | "github.com/quasilyte/gmath" 11 | ) 12 | 13 | const maxInputLen = 10 14 | 15 | type componentInput struct { 16 | pos gmath.Vec 17 | input *input.Handler 18 | pressedRunes []rune 19 | text []byte 20 | lcdLabel *lcdLabel 21 | cursorPos int 22 | cursor *ge.Label 23 | cursorBlinkDelay float64 24 | advancedOps bool 25 | 26 | EventOnTextChanged gesignal.Event[gesignal.Void] 27 | } 28 | 29 | func newComponentInput(h *input.Handler, pos gmath.Vec, advancedOps bool) *componentInput { 30 | return &componentInput{input: h, pos: pos, advancedOps: advancedOps} 31 | } 32 | 33 | func (i *componentInput) SetText(s string) { 34 | i.text = []byte(s) 35 | i.cursorPos = -1 36 | i.onTextChanged() 37 | } 38 | 39 | func (i *componentInput) Init(scene *ge.Scene) { 40 | i.text = []byte("abc") 41 | textColor := ge.RGB(0x2eb43c) 42 | i.lcdLabel = newLCDLabel(i.pos, textColor, string(i.text)) 43 | scene.AddObject(i.lcdLabel) 44 | 45 | i.cursor = scene.NewLabel(FontLCDSmall) 46 | i.cursor.Visible = false 47 | i.cursor.Height = 64 48 | i.cursor.Pos.Base = &i.pos 49 | i.cursor.Pos.Offset.Y += 6 50 | i.cursor.Text = "_" 51 | i.cursor.AlignVertical = ge.AlignVerticalCenter 52 | i.cursor.ColorScale.SetColor(textColor) 53 | scene.AddGraphics(i.cursor) 54 | 55 | i.cursorPos = -1 56 | i.cursorBlinkDelay = 0.2 57 | 58 | i.onTextChanged() 59 | } 60 | 61 | func (i *componentInput) IsDisposed() bool { return false } 62 | 63 | func (i *componentInput) Update(delta float64) { 64 | i.cursorBlinkDelay -= delta 65 | if i.cursorBlinkDelay <= 0 { 66 | i.cursorBlinkDelay = 1 67 | i.cursor.Visible = !i.cursor.Visible 68 | } 69 | 70 | if i.advancedOps && len(i.text) != 0 { 71 | if i.cursorPos != -1 && i.input.ActionIsJustPressed(ActionCharInc) { 72 | i.text[i.cursorPos] = incChar(i.text[i.cursorPos]) 73 | i.onTextChanged() 74 | return 75 | } 76 | if i.cursorPos != -1 && i.input.ActionIsJustPressed(ActionCharDec) { 77 | i.text[i.cursorPos] = decChar(i.text[i.cursorPos]) 78 | i.onTextChanged() 79 | return 80 | } 81 | if i.input.ActionIsJustPressed(ActionRotateLeft) { 82 | rotateCharsLeft(i.text) 83 | i.onTextChanged() 84 | return 85 | } 86 | if i.input.ActionIsJustPressed(ActionRotateRight) { 87 | rotateCharsRight(i.text) 88 | i.onTextChanged() 89 | return 90 | } 91 | } 92 | 93 | if len(i.text) != 0 && (i.cursorPos > 0 || i.cursorPos == -1) && i.input.ActionIsJustPressed(ActionCursorLeft) { 94 | if i.cursorPos == -1 { 95 | i.cursorPos = len(i.text) - 1 96 | } else { 97 | i.cursorPos-- 98 | } 99 | i.cursorBlinkDelay = 0.5 100 | i.cursor.Visible = true 101 | i.onCursorChanged() 102 | } 103 | if i.cursorPos != -1 && i.input.ActionIsJustPressed(ActionCursorRight) { 104 | i.cursorPos++ 105 | i.cursorBlinkDelay = 0.5 106 | i.cursor.Visible = true 107 | i.onCursorChanged() 108 | if i.cursorPos >= len(i.text) { 109 | i.cursorPos = -1 110 | } 111 | } 112 | if len(i.text) != 0 && i.input.ActionIsJustPressed(ActionRemoveCurrentChar) { 113 | if i.cursorPos != -1 { 114 | deletePos := i.cursorPos 115 | i.text = append(i.text[:deletePos], i.text[deletePos+1:]...) 116 | i.onTextChanged() 117 | if len(i.text) == 0 || i.cursorPos == len(i.text) { 118 | i.cursorPos = -1 119 | } 120 | return 121 | } 122 | } 123 | if len(i.text) != 0 && i.input.ActionIsJustPressed(ActionRemovePrevChar) { 124 | if i.cursorPos != 0 { 125 | if i.cursorPos == -1 { 126 | // Remove the last letter. 127 | i.text = i.text[:len(i.text)-1] 128 | } else { 129 | // Remove the letter behind the cursor. 130 | deletePos := i.cursorPos - 1 131 | i.text = append(i.text[:deletePos], i.text[deletePos+1:]...) 132 | i.cursorPos-- 133 | } 134 | i.onTextChanged() 135 | return 136 | } 137 | } 138 | if len(i.text) < maxInputLen && !ebiten.IsKeyPressed(ebiten.KeyControl) { 139 | i.pressedRunes = ebiten.AppendInputChars(i.pressedRunes[:0]) 140 | if len(i.pressedRunes) != 0 { 141 | changed := false 142 | for _, r := range i.pressedRunes { 143 | if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' { 144 | r = unicode.ToLower(r) 145 | if i.cursorPos == -1 { 146 | // Append the char to the end of the text. 147 | i.text = append(i.text, byte(r)) 148 | } else { 149 | // Prepend the char. 150 | insertPos := i.cursorPos 151 | i.text = append(i.text[:insertPos], append([]byte{byte(r)}, i.text[insertPos:]...)...) 152 | i.cursorPos++ 153 | } 154 | changed = true 155 | } 156 | } 157 | if changed { 158 | i.onTextChanged() 159 | } 160 | } 161 | } 162 | } 163 | 164 | func (i *componentInput) onTextChanged() { 165 | i.lcdLabel.text = string(i.text) 166 | i.onCursorChanged() 167 | i.EventOnTextChanged.Emit(gesignal.Void{}) 168 | } 169 | 170 | func (i *componentInput) onCursorChanged() { 171 | cursorPos := i.cursorPos 172 | if cursorPos < 0 { 173 | cursorPos = len(i.text) 174 | } 175 | const letterWidth = 26 176 | textWidth := letterWidth * len(i.text) 177 | firstLetterOffset := 160 - textWidth/2 178 | i.cursor.Pos.Offset.X = float64(firstLetterOffset + cursorPos*letterWidth + 2) 179 | } 180 | -------------------------------------------------------------------------------- /src/_assets/levels/story/rinse_repeat.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":288, 20 | "y":480 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":384, 32 | "y":480 33 | }, 34 | { 35 | "class":"", 36 | "gid":5, 37 | "height":96, 38 | "id":114, 39 | "name":"", 40 | "rotation":270, 41 | "visible":true, 42 | "width":96, 43 | "x":576, 44 | "y":384 45 | }, 46 | { 47 | "class":"", 48 | "gid":12, 49 | "height":96, 50 | "id":115, 51 | "name":"", 52 | "rotation":0, 53 | "visible":true, 54 | "width":96, 55 | "x":480, 56 | "y":288 57 | }, 58 | { 59 | "class":"", 60 | "gid":21, 61 | "height":96, 62 | "id":116, 63 | "name":"", 64 | "properties":[ 65 | { 66 | "name":"keywords", 67 | "type":"string", 68 | "value":"gecko\nkite\nisland\nsnowman\nbishop\ncrop\nshoe" 69 | }, 70 | { 71 | "name":"num_keywords", 72 | "type":"int", 73 | "value":2 74 | }], 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":0, 79 | "y":768 80 | }, 81 | { 82 | "class":"", 83 | "gid":7, 84 | "height":96, 85 | "id":118, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":576, 91 | "y":480 92 | }, 93 | { 94 | "class":"", 95 | "gid":8, 96 | "height":96, 97 | "id":120, 98 | "name":"", 99 | "rotation":180, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":480 104 | }, 105 | { 106 | "class":"", 107 | "gid":33, 108 | "height":96, 109 | "id":122, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":480, 115 | "y":480 116 | }, 117 | { 118 | "class":"", 119 | "gid":15, 120 | "height":96, 121 | "id":123, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":576, 127 | "y":576 128 | }, 129 | { 130 | "class":"", 131 | "gid":57, 132 | "height":96, 133 | "id":124, 134 | "name":"", 135 | "properties":[ 136 | { 137 | "name":"text", 138 | "type":"string", 139 | "value":"the element icon\ndescribes its function" 140 | }], 141 | "rotation":0, 142 | "visible":true, 143 | "width":96, 144 | "x":864, 145 | "y":192 146 | }, 147 | { 148 | "class":"", 149 | "gid":57, 150 | "height":96, 151 | "id":125, 152 | "name":"", 153 | "properties":[ 154 | { 155 | "name":"text", 156 | "type":"string", 157 | "value":"the target INPUT is\nalways a valid word" 158 | }], 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":864, 163 | "y":384 164 | }], 165 | "opacity":1, 166 | "type":"objectgroup", 167 | "visible":true, 168 | "x":0, 169 | "y":0 170 | }], 171 | "nextlayerid":3, 172 | "nextobjectid":126, 173 | "orientation":"orthogonal", 174 | "renderorder":"right-down", 175 | "tiledversion":"1.9.2", 176 | "tileheight":96, 177 | "tilesets":[ 178 | { 179 | "firstgid":1, 180 | "source":"..\/..\/schemas.tsj" 181 | }], 182 | "tilewidth":96, 183 | "type":"map", 184 | "version":"1.9", 185 | "width":12 186 | } -------------------------------------------------------------------------------- /src/level_select_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/quasilyte/decipherism-game/leveldata" 8 | "github.com/quasilyte/ge" 9 | "github.com/quasilyte/ge/tiled" 10 | "github.com/quasilyte/ge/ui" 11 | "github.com/quasilyte/ge/xslices" 12 | "github.com/quasilyte/gmath" 13 | ) 14 | 15 | type levelSelectController struct { 16 | gameState *gameState 17 | scene *ge.Scene 18 | secretKeywords []string 19 | } 20 | 21 | func newLevelSelectController(s *gameState) *levelSelectController { 22 | return &levelSelectController{gameState: s} 23 | } 24 | 25 | func (c *levelSelectController) Init(scene *ge.Scene) { 26 | c.scene = scene 27 | 28 | if c.gameState.data.Options.MusicVolumeLevel != 0 { 29 | scene.Audio().ContinueMusic(AudioMenuMusic) 30 | } 31 | 32 | bg := scene.NewSprite(ImagePaperBg) 33 | bg.Centered = false 34 | scene.AddGraphics(bg) 35 | 36 | layer := ge.NewShaderLayer() 37 | layer.Shader = scene.NewShader(ShaderHandwriting) 38 | 39 | chapter := c.gameState.chapter 40 | levelStrings := make([]string, len(chapter.levels)) 41 | for i := range c.gameState.chapter.levels { 42 | if chapter.IsBonus() { 43 | levelStrings[i] = fmt.Sprintf("[ ] Component %d", i+1) 44 | } else { 45 | levelStrings[i] = fmt.Sprintf("[ ][ ] Component %d", i+1) 46 | } 47 | } 48 | 49 | var encodedKeyword string 50 | c.secretKeywords = make([]string, len(chapter.levels)) 51 | if !chapter.IsBonus() { 52 | tileset, err := tiled.UnmarshalTileset(scene.LoadRaw(RawComponentSchemaTilesetJSON).Data) 53 | if err != nil { 54 | panic(err) 55 | } 56 | runner := newSchemaRunner() 57 | inputData := []byte(chapter.keyword) 58 | for i, levelName := range chapter.levels { 59 | level := theStoryModeMap.levels[levelName] 60 | levelData := scene.LoadRaw(level.id).Data 61 | schema := leveldata.DecodeSchema(gmath.Vec{}, tileset, levelData) 62 | completionData := c.gameState.GetLevelCompletionData(levelName) 63 | if completionData != nil && completionData.SecretKeyword { 64 | levelStrings[i] += " (" + strings.ToUpper(string(inputData)) + ")" 65 | } 66 | c.secretKeywords[i] = string(inputData) // A non-encoded input 67 | inputData = []byte(runner.Exec(schema, string(inputData))) 68 | } 69 | encodedKeyword = strings.ToUpper(string(inputData)) 70 | } 71 | labelText := "Block " + c.gameState.chapter.label + "\n\n" + strings.Join(levelStrings, "\n\n") 72 | if !chapter.IsBonus() { 73 | labelText += "\n\n" + encodedKeyword 74 | } 75 | offset := gmath.Vec{X: 322, Y: 76} 76 | l := scene.NewLabel(FontHandwritten) 77 | l.Text = labelText 78 | l.Pos.Offset = offset 79 | l.ColorScale.SetRGBA(30, 30, 60, 220) 80 | layer.AddGraphics(l) 81 | 82 | c.initUI(offset) 83 | 84 | scene.AddGraphics(layer) 85 | } 86 | 87 | func (c *levelSelectController) initDecipherConfig(content contentStatus, config *decipherConfig) { 88 | config.terminalUpgrades.valueInspector = xslices.Contains(content.techLevelFeatures, "value inspector") 89 | config.terminalUpgrades.textBuffer = xslices.Contains(content.techLevelFeatures, "text buffer") 90 | config.terminalUpgrades.branchingInfo = xslices.Contains(content.techLevelFeatures, "branching info") 91 | config.terminalUpgrades.ioLog = xslices.Contains(content.techLevelFeatures, "i/o logs") 92 | config.terminalUpgrades.outputPredictor = xslices.Contains(content.techLevelFeatures, "output predictor") 93 | config.advancedInput = xslices.Contains(content.techLevelFeatures, "advanced input") 94 | } 95 | 96 | func (c *levelSelectController) initUI(offset gmath.Vec) { 97 | uiRoot := ui.NewRoot(c.scene.Context(), c.gameState.input) 98 | uiRoot.ActivationAction = ActionMenuConfirm 99 | uiRoot.NextInputAction = ActionMenuNext 100 | uiRoot.PrevInputAction = ActionMenuPrev 101 | c.scene.AddObject(uiRoot) 102 | 103 | offset = offset.Add(gmath.Vec{X: 192, Y: 156}) 104 | 105 | var bgroup buttonGroup 106 | chapter := c.gameState.chapter 107 | for i, levelName := range chapter.levels { 108 | level := theStoryModeMap.levels[levelName] 109 | secretKeyword := c.secretKeywords[i] 110 | b := uiRoot.NewButton(outlineButtonStyle.Resized(454, 80)) 111 | bgroup.AddButton(b) 112 | b.EventActivated.Connect(nil, func(_ *ui.Button) { 113 | content := calculateContentStatus(c.gameState) 114 | c.gameState.level = level 115 | c.gameState.content = content 116 | config := decipherConfig{ 117 | secretKeyword: secretKeyword, 118 | storyMode: true, 119 | } 120 | c.initDecipherConfig(content, &config) 121 | levelTemplate, err := loadLevelTemplate(c.scene, c.scene.LoadRaw(c.gameState.level.id).Data) 122 | if err != nil { 123 | panic(err) // Builtin level should never contain any errors 124 | } 125 | config.levelTemplate = levelTemplate 126 | c.scene.Context().ChangeScene(newDecipherController(c.gameState, config)) 127 | }) 128 | completionData := c.gameState.GetLevelCompletionData(levelName) 129 | if completionData != nil { 130 | checkmark := c.scene.NewSprite(ImageCompleteMark) 131 | checkmark.Pos.Offset = offset.Add(gmath.Vec{X: -154, Y: 40}) 132 | c.scene.AddGraphics(checkmark) 133 | if !chapter.IsBonus() && completionData.SecretKeyword { 134 | checkmark2 := c.scene.NewSprite(ImageCompleteMark) 135 | checkmark2.Pos.Offset = offset.Add(gmath.Vec{X: -70, Y: 40}) 136 | c.scene.AddGraphics(checkmark2) 137 | } 138 | } 139 | b.Pos.Offset = offset 140 | c.scene.AddObject(b) 141 | if !chapter.IsBonus() { 142 | arrow := c.scene.NewSprite(ImagePipelineArrow) 143 | arrow.Pos.Offset = offset.Add(gmath.Vec{X: -236, Y: 112}) 144 | c.scene.AddGraphics(arrow) 145 | } 146 | offset.Y += 166 147 | } 148 | bgroup.Connect(uiRoot) 149 | bgroup.FocusFirst() 150 | } 151 | 152 | func (c *levelSelectController) Update(delta float64) { 153 | if c.gameState.input.ActionIsJustPressed(ActionLeave) { 154 | c.leave() 155 | return 156 | } 157 | } 158 | 159 | func (c *levelSelectController) leave() { 160 | c.scene.Context().ChangeScene(newChapterSelectController(c.gameState)) 161 | } 162 | -------------------------------------------------------------------------------- /src/level_generator.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/quasilyte/ge/tiled" 10 | "github.com/quasilyte/gmath" 11 | ) 12 | 13 | type generatorConfig struct { 14 | difficulty int 15 | } 16 | 17 | func generateLevel(r *gmath.Rand, tileset *tiled.Tileset, config generatorConfig) *schemaTemplate { 18 | g := levelGenerator{rand: r, config: config} 19 | return g.generate(tileset) 20 | } 21 | 22 | type levelGenerator struct { 23 | rand *gmath.Rand 24 | config generatorConfig 25 | result *schemaTemplate 26 | tiles [numSchemaRows][numSchemaCols]generatorTile 27 | } 28 | 29 | type generatorTile uint8 30 | 31 | const ( 32 | gtileUnset generatorTile = iota 33 | gtileOutOfBounds 34 | gtileElem 35 | ) 36 | 37 | func (g *levelGenerator) generate(tileset *tiled.Tileset) *schemaTemplate { 38 | numAttempts := 1 39 | for { 40 | g.result = &schemaTemplate{tileset: tileset} 41 | ok := g.tryGenerate() 42 | if ok { 43 | break 44 | } 45 | g.tiles = [numSchemaRows][numSchemaCols]generatorTile{} 46 | numAttempts++ 47 | } 48 | // Scale the positions: from coord to the actual pixel offsets. 49 | for i := range g.result.elems { 50 | elem := &g.result.elems[i] 51 | // elem.pos.X = elem.pos.X*tileset.TileWidth + tileset.TileWidth/2 52 | // elem.pos.Y = elem.pos.Y*tileset.TileHeight - tileset.TileHeight/2 53 | fmt.Println(elem.class, elem.pos, elem.rotation) 54 | } 55 | fmt.Printf("generation took %d attempts\n", numAttempts) 56 | return g.result 57 | } 58 | 59 | func (g *levelGenerator) tryGenerate() bool { 60 | tileset := g.result.tileset 61 | startCol := float64(g.rand.IntRange(2, numSchemaCols-2-1)) 62 | startRow := float64(g.rand.IntRange(2, numSchemaRows-2-1)) 63 | pos := gmath.Vec{ 64 | X: startCol*tileset.TileWidth + tileset.TileWidth/2, 65 | Y: startRow*tileset.TileHeight + tileset.TileHeight/2, 66 | } 67 | // branchBudget := g.rand.IntRange(3+g.config.difficulty*2, 5+g.config.difficulty*2) 68 | branchBudget := 1 69 | g.deployElem(pos, "elem_input", 0) 70 | startShape := getElemShape("elem_input", pos, 0, nil) 71 | return g.buildBranch(pos, startShape, branchBudget) 72 | } 73 | 74 | func (g *levelGenerator) buildBranch(pos gmath.Vec, srcShape elemShape, budget int) bool { 75 | m := g.pickModule(budget) 76 | if m != nil { 77 | budget -= m.cost 78 | } else { 79 | m = outputModule 80 | } 81 | dirIndex := g.rand.IntRange(0, 3) 82 | for i := 0; i < len(allDirections); i++ { 83 | dir := allDirections[dirIndex] 84 | if exits, ok := g.tryDeployModule(m, srcShape, pos.MoveInDirection(96, dir), dir); ok { 85 | for _, e := range exits { 86 | shape := getElemShape(e.class, e.pos, e.rotation, e.extraData) 87 | if !g.buildBranch(e.pos, shape, budget) { 88 | return false 89 | } 90 | } 91 | return true 92 | } 93 | dirIndex++ 94 | if dirIndex >= len(allDirections) { 95 | dirIndex = 0 96 | } 97 | } 98 | return false 99 | } 100 | 101 | func (g *levelGenerator) tryDeployModule(m *generatorModule, srcShape elemShape, pos gmath.Vec, dir gmath.Rad) ([]schemaTemplateElem, bool) { 102 | // Can this module be connected to a given source shape? 103 | entryElem := m.elems[0] 104 | entryElemPos := entryElem.pos.Add(pos) 105 | entryShape := getElemShape(entryElem.class, entryElemPos, entryElem.rotation, entryElem.extraData) 106 | if !srcShape.CanConnectTo(&entryShape) { 107 | return nil, false 108 | } 109 | // First check if we can deploy all module elements. 110 | for _, proto := range m.elems { 111 | elemPos := proto.pos.Add(pos) 112 | if g.readCell(elemPos) != gtileUnset { 113 | return nil, false 114 | } 115 | } 116 | // Now do the actual deployment. 117 | for _, proto := range m.elems { 118 | elemPos := proto.pos.Add(pos) 119 | g.deployElem(elemPos, proto.class, dir) 120 | } 121 | if m.numExits == 0 { 122 | return nil, true 123 | } 124 | return g.result.elems[len(g.result.elems)-m.numExits:], true 125 | } 126 | 127 | func (g *levelGenerator) pickModule(budget int) *generatorModule { 128 | index := g.rand.IntRange(0, len(generatorModules)-1) 129 | for i := 0; i < len(generatorModules); i++ { 130 | m := &generatorModules[index] 131 | if m.cost <= budget { 132 | return m 133 | } 134 | if index >= len(generatorModules) { 135 | index = 0 136 | } 137 | } 138 | return nil 139 | } 140 | 141 | func (g *levelGenerator) deployElem(pos gmath.Vec, class string, rotation gmath.Rad) { 142 | e := schemaTemplateElem{ 143 | class: class, 144 | pos: pos, 145 | classID: -1, 146 | } 147 | switch class { 148 | case "pipe": 149 | e.rotation = rotation 150 | } 151 | g.result.elems = append(g.result.elems, e) 152 | g.markCell(pos, gtileElem) 153 | } 154 | 155 | func (g *levelGenerator) rowcolByPos(pos gmath.Vec) (int, int) { 156 | col := int(pos.X) / int(g.result.tileset.TileWidth) 157 | row := int(pos.Y) / int(g.result.tileset.TileHeight) 158 | return row, col 159 | } 160 | 161 | func (g *levelGenerator) markCell(pos gmath.Vec, t generatorTile) { 162 | row, col := g.rowcolByPos(pos) 163 | g.tiles[row][col] = t 164 | } 165 | 166 | func (g *levelGenerator) readCell(pos gmath.Vec) generatorTile { 167 | row, col := g.rowcolByPos(pos) 168 | if col >= numSchemaCols || col < 0 { 169 | return gtileOutOfBounds 170 | } 171 | if row >= numSchemaRows || row < 0 { 172 | return gtileOutOfBounds 173 | } 174 | return g.tiles[row][col] 175 | } 176 | 177 | var ( 178 | dirRight = gmath.Rad(0) 179 | dirDown = gmath.DegToRad(90) 180 | dirLeft = gmath.DegToRad(180) 181 | dirUp = gmath.DegToRad(270) 182 | ) 183 | 184 | var allDirections = []gmath.Rad{ 185 | dirRight, 186 | dirDown, 187 | dirLeft, 188 | dirUp, 189 | } 190 | 191 | type generatorModule struct { 192 | cost int 193 | elems []schemaTemplateElem 194 | numExits int 195 | } 196 | 197 | var outputModule = &generatorModule{ 198 | numExits: 0, 199 | elems: []schemaTemplateElem{ 200 | {class: "elem_output"}, 201 | }, 202 | } 203 | 204 | var generatorModules = []generatorModule{ 205 | { 206 | cost: 1, 207 | numExits: 1, 208 | elems: []schemaTemplateElem{ 209 | {class: "pipe"}, 210 | }, 211 | }, 212 | } 213 | -------------------------------------------------------------------------------- /src/game_state.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | resource "github.com/quasilyte/ebitengine-resource" 7 | "github.com/quasilyte/ge/input" 8 | "github.com/quasilyte/ge/xslices" 9 | "github.com/quasilyte/gmath" 10 | ) 11 | 12 | type gameState struct { 13 | input *input.Handler 14 | chapter *storyModeChapter 15 | level storyModeLevel 16 | data *persistentGameData 17 | content contentStatus 18 | userFolder string 19 | } 20 | 21 | type chapterCompletionData struct { 22 | partiallyCompleted bool 23 | allLevelsCompleted bool 24 | fullyCompleted bool 25 | secretDecoded bool 26 | } 27 | 28 | func (state *gameState) GetLevelCompletionData(name string) *completedLevelData { 29 | return xslices.Find(state.data.CompletedLevels, func(l *completedLevelData) bool { 30 | return l.Name == name 31 | }) 32 | } 33 | 34 | func (state *gameState) GetChapterCompletionData(c *storyModeChapter) chapterCompletionData { 35 | var result chapterCompletionData 36 | levelsCompleted := 0 37 | keywordsSolved := 0 38 | for i, levelName := range c.levels { 39 | levelData := xslices.Find(state.data.CompletedLevels, func(l *completedLevelData) bool { 40 | return l.Name == levelName 41 | }) 42 | if levelData != nil { 43 | levelsCompleted++ 44 | if levelData.SecretKeyword { 45 | keywordsSolved++ 46 | if i == 0 { 47 | result.secretDecoded = true 48 | } 49 | } 50 | } 51 | } 52 | result.fullyCompleted = levelsCompleted == len(c.levels) 53 | if !c.IsBonus() { 54 | result.fullyCompleted = result.fullyCompleted && keywordsSolved == len(c.levels) 55 | } 56 | result.allLevelsCompleted = levelsCompleted == len(c.levels) 57 | result.partiallyCompleted = levelsCompleted != 0 && levelsCompleted >= (len(c.levels)-1) 58 | return result 59 | } 60 | 61 | type persistentGameData struct { 62 | CompletedLevels []completedLevelData 63 | SolvedAtbash bool 64 | SolvedRot13 bool 65 | SolvedIncDec bool 66 | SolvedShift bool 67 | SolvedPolygraphic bool 68 | SolvedNegation bool 69 | SolvedCondTransform bool 70 | UsedCheats bool 71 | UsedHiddenKeybinds bool 72 | SawCollision bool 73 | CompletionTime time.Duration 74 | Options gameOptions 75 | } 76 | 77 | type gameOptions struct { 78 | MusicVolumeLevel int 79 | EffectsVolumeLevel int 80 | CrtShader bool 81 | } 82 | 83 | type completedLevelData struct { 84 | Name string 85 | SecretKeyword bool 86 | } 87 | 88 | type storyModeMap struct { 89 | chapters []storyModeChapter 90 | levels map[string]storyModeLevel 91 | } 92 | 93 | func (m *storyModeMap) getChapter(name string) *storyModeChapter { 94 | return xslices.Find(m.chapters, func(c *storyModeChapter) bool { 95 | return c.name == name 96 | }) 97 | } 98 | 99 | type storyModeChapter struct { 100 | name string 101 | label string 102 | keyword string 103 | levels []string 104 | requires string 105 | gridPos gmath.Vec 106 | } 107 | 108 | func (c *storyModeChapter) IsBonus() bool { return c.keyword == "" } 109 | 110 | type storyModeLevel struct { 111 | name string 112 | id resource.RawID 113 | } 114 | 115 | var theStoryModeMap = &storyModeMap{ 116 | chapters: []storyModeChapter{ 117 | { 118 | label: "1+", 119 | name: "bonus1", 120 | requires: "story1", 121 | levels: []string{ 122 | "double_negation", 123 | "spellbook", 124 | "lossy_conversion", 125 | }, 126 | gridPos: gmath.Vec{X: 0, Y: 1}, 127 | }, 128 | { 129 | label: "2+", 130 | name: "bonus2", 131 | requires: "story2", 132 | levels: []string{ 133 | "polygraphic_atbash", 134 | "sub_loop", 135 | "branchless_encoder", 136 | }, 137 | gridPos: gmath.Vec{X: 1, Y: 1}, 138 | }, 139 | { 140 | label: "3+", 141 | name: "bonus3", 142 | requires: "story3", 143 | levels: []string{ 144 | "symmetry", 145 | "double_zigzag", 146 | "deduction", 147 | }, 148 | gridPos: gmath.Vec{X: 2, Y: 1}, 149 | }, 150 | { 151 | label: "4+", 152 | name: "bonus4", 153 | requires: "story4", 154 | levels: []string{ 155 | "clear_head", 156 | "even_odd_add", 157 | "rumble", 158 | }, 159 | gridPos: gmath.Vec{X: 3, Y: 1}, 160 | }, 161 | { 162 | label: "5+", 163 | name: "bonus5", 164 | requires: "story5", 165 | levels: []string{ 166 | "claws", 167 | "stuttering", 168 | "the_best_number", 169 | }, 170 | gridPos: gmath.Vec{X: 4, Y: 1}, 171 | }, 172 | { 173 | label: "6+", 174 | name: "bonus6", 175 | requires: "story6", 176 | levels: []string{ 177 | "conveyor", 178 | "pyramid", 179 | "mission_impossible", 180 | }, 181 | gridPos: gmath.Vec{X: 3, Y: 2}, 182 | }, 183 | 184 | { 185 | label: "1", 186 | name: "story1", 187 | keyword: "rain", 188 | levels: []string{ 189 | "hello_world", 190 | "rinse_repeat", 191 | "add_or_sub", 192 | }, 193 | gridPos: gmath.Vec{X: 0, Y: 0}, 194 | }, 195 | { 196 | label: "2", 197 | name: "story2", 198 | requires: "story1", 199 | keyword: "storm", 200 | levels: []string{ 201 | "vowel_shifter", 202 | "efforts_negated", 203 | "addsub_negation", 204 | }, 205 | gridPos: gmath.Vec{X: 1, Y: 0}, 206 | }, 207 | { 208 | label: "3", 209 | name: "story3", 210 | requires: "story2", 211 | keyword: "thunder", 212 | levels: []string{ 213 | "atbash", 214 | "swap_shifter", 215 | "determination", 216 | }, 217 | gridPos: gmath.Vec{X: 2, Y: 0}, 218 | }, 219 | { 220 | label: "4", 221 | name: "story4", 222 | requires: "story3", 223 | keyword: "tsunami", 224 | levels: []string{ 225 | "ladder", 226 | "red_herring", 227 | "binary_tree", 228 | }, 229 | gridPos: gmath.Vec{X: 3, Y: 0}, 230 | }, 231 | { 232 | label: "5", 233 | name: "story5", 234 | requires: "story4", 235 | keyword: "whirlwind", 236 | levels: []string{ 237 | "switch", 238 | "dotmask", 239 | "odd_evening", 240 | "nop_shuffling", 241 | }, 242 | gridPos: gmath.Vec{X: 4, Y: 0}, 243 | }, 244 | { 245 | label: "6", 246 | name: "story6", 247 | requires: "story5", 248 | keyword: "cloudburst", 249 | levels: []string{ 250 | "single_key", 251 | "rot13", 252 | "fixed_cond", 253 | "spiral", 254 | }, 255 | gridPos: gmath.Vec{X: 4, Y: 2}, 256 | }, 257 | }, 258 | } 259 | -------------------------------------------------------------------------------- /src/_assets/levels/story/branchless_encoder.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":192, 20 | "y":288 21 | }, 22 | { 23 | "class":"", 24 | "gid":12, 25 | "height":96, 26 | "id":115, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":384, 32 | "y":480 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"pumpkin\nconfusion\ncampaign\ncorporate\nsaturn" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":2 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":5, 60 | "height":96, 61 | "id":117, 62 | "name":"", 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":288, 67 | "y":288 68 | }, 69 | { 70 | "class":"", 71 | "gid":27, 72 | "height":96, 73 | "id":118, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":384, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":5, 84 | "height":96, 85 | "id":121, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":480, 91 | "y":288 92 | }, 93 | { 94 | "class":"", 95 | "gid":13, 96 | "height":96, 97 | "id":122, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":288 104 | }, 105 | { 106 | "class":"", 107 | "gid":5, 108 | "height":96, 109 | "id":123, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":672, 115 | "y":288 116 | }, 117 | { 118 | "class":"", 119 | "gid":13, 120 | "height":96, 121 | "id":124, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":768, 127 | "y":288 128 | }, 129 | { 130 | "class":"", 131 | "gid":5, 132 | "height":96, 133 | "id":126, 134 | "name":"", 135 | "rotation":90, 136 | "visible":true, 137 | "width":96, 138 | "x":768, 139 | "y":288 140 | }, 141 | { 142 | "class":"", 143 | "gid":27, 144 | "height":96, 145 | "id":127, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":768, 151 | "y":480 152 | }, 153 | { 154 | "class":"", 155 | "gid":5, 156 | "height":96, 157 | "id":128, 158 | "name":"", 159 | "rotation":180, 160 | "visible":true, 161 | "width":96, 162 | "x":768, 163 | "y":384 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":4, 169 | "height":96, 170 | "id":129, 171 | "name":"", 172 | "rotation":0, 173 | "visible":true, 174 | "width":96, 175 | "x":576, 176 | "y":480 177 | }, 178 | { 179 | "class":"", 180 | "gid":5, 181 | "height":96, 182 | "id":130, 183 | "name":"", 184 | "rotation":180, 185 | "visible":true, 186 | "width":96, 187 | "x":576, 188 | "y":384 189 | }], 190 | "opacity":1, 191 | "type":"objectgroup", 192 | "visible":true, 193 | "x":0, 194 | "y":0 195 | }], 196 | "nextlayerid":3, 197 | "nextobjectid":131, 198 | "orientation":"orthogonal", 199 | "renderorder":"right-down", 200 | "tiledversion":"1.9.2", 201 | "tileheight":96, 202 | "tilesets":[ 203 | { 204 | "firstgid":1, 205 | "source":"..\/..\/schemas.tsj" 206 | }], 207 | "tilewidth":96, 208 | "type":"map", 209 | "version":"1.9", 210 | "width":12 211 | } -------------------------------------------------------------------------------- /src/_assets/levels/story/dotmask.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":96, 20 | "y":480 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":192, 32 | "y":480 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"trace\nreact\ncaret\ncrate\nlimbo\nsymbol\ncompiler\n" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":3 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":45, 60 | "height":96, 61 | "id":118, 62 | "name":"", 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":480, 67 | "y":480 68 | }, 69 | { 70 | "class":"", 71 | "gid":5, 72 | "height":96, 73 | "id":119, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":384, 79 | "y":480 80 | }, 81 | { 82 | "class":"", 83 | "gid":47, 84 | "height":96, 85 | "id":120, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":672, 91 | "y":480 92 | }, 93 | { 94 | "class":"", 95 | "gid":5, 96 | "height":96, 97 | "id":121, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":480 104 | }, 105 | { 106 | "class":"", 107 | "gid":7, 108 | "height":96, 109 | "id":123, 110 | "name":"", 111 | "rotation":270, 112 | "visible":true, 113 | "width":96, 114 | "x":384, 115 | "y":384 116 | }, 117 | { 118 | "class":"", 119 | "gid":8, 120 | "height":96, 121 | "id":124, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":480, 127 | "y":384 128 | }, 129 | { 130 | "class":"", 131 | "gid":12, 132 | "height":96, 133 | "id":125, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":864, 139 | "y":480 140 | }, 141 | { 142 | "class":"", 143 | "gid":5, 144 | "height":96, 145 | "id":126, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":768, 151 | "y":480 152 | }, 153 | { 154 | "class":"", 155 | "gid":4, 156 | "height":96, 157 | "id":127, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":384, 163 | "y":384 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":3, 169 | "height":96, 170 | "id":130, 171 | "name":"", 172 | "properties":[ 173 | { 174 | "name":"cond_kind", 175 | "type":"string", 176 | "value":"anagram" 177 | }, 178 | { 179 | "name":"string_arg", 180 | "type":"string", 181 | "value":"trace" 182 | }], 183 | "rotation":0, 184 | "visible":true, 185 | "width":96, 186 | "x":288, 187 | "y":480 188 | }], 189 | "opacity":1, 190 | "type":"objectgroup", 191 | "visible":true, 192 | "x":0, 193 | "y":0 194 | }], 195 | "nextlayerid":3, 196 | "nextobjectid":131, 197 | "orientation":"orthogonal", 198 | "renderorder":"right-down", 199 | "tiledversion":"1.9.2", 200 | "tileheight":96, 201 | "tilesets":[ 202 | { 203 | "firstgid":1, 204 | "source":"..\/..\/schemas.tsj" 205 | }], 206 | "tilewidth":96, 207 | "type":"map", 208 | "version":"1.9", 209 | "width":12 210 | } -------------------------------------------------------------------------------- /src/custom_level_select_controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/quasilyte/decipherism-game/leveldata" 14 | "github.com/quasilyte/ge" 15 | "github.com/quasilyte/ge/tiled" 16 | "github.com/quasilyte/ge/ui" 17 | "github.com/quasilyte/gmath" 18 | ) 19 | 20 | type customLevelSelectController struct { 21 | gameState *gameState 22 | 23 | scene *ge.Scene 24 | 25 | levelSlider gmath.Slider 26 | allFilenames []string 27 | levelButtons []*levelButton 28 | 29 | totalCounter *ge.Label 30 | } 31 | 32 | type levelButton struct { 33 | node *ui.Button 34 | fileIndex int 35 | } 36 | 37 | func newCustomLevelSelectController(gameState *gameState) *customLevelSelectController { 38 | return &customLevelSelectController{gameState: gameState} 39 | } 40 | 41 | func (c *customLevelSelectController) Init(scene *ge.Scene) { 42 | c.scene = scene 43 | ctx := scene.Context() 44 | 45 | buttonWidth := 1024.0 46 | offset := gmath.Vec{X: ctx.WindowWidth/2 - buttonWidth/2, Y: 164} 47 | var bgroup buttonGroup 48 | 49 | l := scene.NewLabel(FontLCDTiny) 50 | l.ColorScale.SetColor(defaultLCDColor) 51 | l.Pos.Offset = gmath.Vec{Y: 100} 52 | if c.gameState.userFolder != "" { 53 | l.Text = "scanning '" + c.gameState.userFolder + "' for levels" 54 | } else { 55 | l.Text = "$DECIPHERISM_DATA is unset" 56 | } 57 | l.Width = ctx.WindowWidth 58 | l.AlignHorizontal = ge.AlignHorizontalCenter 59 | scene.AddGraphics(l) 60 | 61 | uiRoot := ui.NewRoot(ctx, c.gameState.input) 62 | uiRoot.ActivationAction = ActionMenuConfirm 63 | uiRoot.NextInputAction = ActionMenuNext 64 | uiRoot.PrevInputAction = ActionMenuPrev 65 | scene.AddObject(uiRoot) 66 | 67 | allFilenames, err := c.scanCustomLevels() 68 | if err != nil { 69 | if errors.Is(err, fs.ErrNotExist) { 70 | l.Text = fmt.Sprintf("scan '%s/levels' for levels: non-existing path", c.gameState.userFolder) 71 | } else { 72 | l.Text = fmt.Sprintf("scan $DECIPHERISM_DATA: %v", err) 73 | } 74 | } 75 | c.allFilenames = allFilenames 76 | c.levelSlider.SetBounds(0, len(c.allFilenames)-1) 77 | 78 | for i := 0; i < 5; i++ { 79 | b := uiRoot.NewButton(optionsButtonStyle.Resized(buttonWidth, 80)) 80 | bgroup.AddButton(b) 81 | buttonIndex := i 82 | b.EventActivated.Connect(nil, func(b *ui.Button) { 83 | fileIndex := c.levelButtons[buttonIndex].fileIndex 84 | selectedFilename := c.allFilenames[fileIndex] 85 | levelData, err := os.ReadFile(selectedFilename) 86 | if err != nil { 87 | panic(err) // TODO: better error handling 88 | } 89 | levelTemplate, err := loadLevelTemplate(c.scene, levelData) 90 | if err != nil { 91 | panic(err) // Should be already verified by this moment 92 | } 93 | config := decipherConfig{ 94 | levelTemplate: levelTemplate, 95 | } 96 | c.scene.Context().ChangeScene(newDecipherController(c.gameState, config)) 97 | }) 98 | c.levelButtons = append(c.levelButtons, &levelButton{ 99 | node: b, 100 | }) 101 | b.Pos.Offset = offset 102 | scene.AddObject(b) 103 | offset.Y += 128 104 | } 105 | 106 | scrollButtonWidth := 320.0 107 | 108 | scrollBackButton := uiRoot.NewButton(optionsButtonStyle.Resized(scrollButtonWidth, 80)) 109 | bgroup.AddButton(scrollBackButton) 110 | scrollBackButton.Text = "<" 111 | scrollBackButton.Pos.Offset = offset 112 | scrollBackButton.EventActivated.Connect(nil, func(_ *ui.Button) { 113 | c.updateSelectionPage() 114 | }) 115 | scene.AddObject(scrollBackButton) 116 | 117 | scrollNextButton := uiRoot.NewButton(optionsButtonStyle.Resized(scrollButtonWidth, 80)) 118 | bgroup.AddButton(scrollNextButton) 119 | scrollNextButton.Text = ">" 120 | scrollNextButton.Pos.Offset = offset.Add(gmath.Vec{X: +(buttonWidth - scrollButtonWidth)}) 121 | scrollNextButton.EventActivated.Connect(nil, func(_ *ui.Button) { 122 | c.updateSelectionPage() 123 | }) 124 | scene.AddObject(scrollNextButton) 125 | 126 | c.totalCounter = scene.NewLabel(FontLCDSmall) 127 | c.totalCounter.ColorScale.SetColor(defaultLCDColor) 128 | c.totalCounter.Width = buttonWidth 129 | c.totalCounter.Height = 80 130 | c.totalCounter.Pos.Offset = offset 131 | c.totalCounter.AlignHorizontal = ge.AlignHorizontalCenter 132 | c.totalCounter.AlignVertical = ge.AlignVerticalCenter 133 | c.totalCounter.Text = fmt.Sprintf("%d levels", len(allFilenames)) 134 | scene.AddGraphics(c.totalCounter) 135 | 136 | offset.Y += 128 137 | 138 | backButtonWidth := 480.0 139 | backButton := uiRoot.NewButton(optionsButtonStyle.Resized(backButtonWidth, 80)) 140 | bgroup.AddButton(backButton) 141 | backButton.Text = "back" 142 | backButton.Pos.Offset = offset.Add(gmath.Vec{X: (buttonWidth - backButtonWidth) / 2}) 143 | backButton.EventActivated.Connect(nil, func(_ *ui.Button) { 144 | c.leave() 145 | }) 146 | scene.AddObject(backButton) 147 | offset.Y += 128 148 | 149 | bgroup.Connect(uiRoot) 150 | bgroup.FocusFirst() 151 | 152 | c.updateSelectionPage() 153 | } 154 | 155 | func (c *customLevelSelectController) Update(delta float64) { 156 | if c.gameState.input.ActionIsJustPressed(ActionLeave) { 157 | c.leave() 158 | return 159 | } 160 | } 161 | 162 | func (c *customLevelSelectController) leave() { 163 | c.scene.Context().ChangeScene(newMainMenuController(c.gameState)) 164 | } 165 | 166 | func (c *customLevelSelectController) updateSelectionPage() { 167 | for i, b := range c.levelButtons { 168 | b.fileIndex = c.levelSlider.Value() 169 | c.levelSlider.Inc() 170 | if i >= len(c.allFilenames) { 171 | b.node.Text = "empty" 172 | b.node.SetDisabled(true) 173 | continue 174 | } 175 | b.node.SetDisabled(false) 176 | filename := c.allFilenames[b.fileIndex] 177 | name := strings.TrimSuffix(filepath.Base(filename), ".json") 178 | name = strings.ReplaceAll(name, "_", " ") 179 | labelText := strconv.Itoa(b.fileIndex+1) + ". " + name 180 | if len(labelText) > 26 { 181 | labelText = labelText[:26] + "..." 182 | } 183 | b.node.Text = labelText 184 | } 185 | } 186 | 187 | func (c *customLevelSelectController) scanCustomLevels() ([]string, error) { 188 | levelsPath := filepath.Join(c.gameState.userFolder, "levels") 189 | 190 | var result []string 191 | files, err := os.ReadDir(levelsPath) 192 | if err != nil { 193 | return nil, err 194 | } 195 | tileset, err := tiled.UnmarshalTileset(c.scene.LoadRaw(RawComponentSchemaTilesetJSON).Data) 196 | if err != nil { 197 | panic(err) 198 | } 199 | for _, f := range files { 200 | if !strings.HasSuffix(f.Name(), ".json") { 201 | continue 202 | } 203 | fullName := filepath.Join(levelsPath, f.Name()) 204 | data, err := os.ReadFile(fullName) 205 | if err != nil { 206 | fmt.Printf("[ERROR] load %q: %v\n", f.Name(), err) 207 | continue 208 | } 209 | if err := leveldata.ValidateLevelData(tileset, data); err != nil { 210 | fmt.Printf("[ERROR] load %q: %v\n", f.Name(), err) 211 | continue 212 | } 213 | result = append(result, fullName) 214 | } 215 | 216 | sort.Strings(result) 217 | 218 | return result, nil 219 | } 220 | -------------------------------------------------------------------------------- /src/_assets/levels/bonus/polygraphic_atbash.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":480, 20 | "y":288 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":576, 32 | "y":288 33 | }, 34 | { 35 | "class":"", 36 | "gid":12, 37 | "height":96, 38 | "id":115, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":480, 44 | "y":480 45 | }, 46 | { 47 | "class":"", 48 | "gid":21, 49 | "height":96, 50 | "id":116, 51 | "name":"", 52 | "properties":[ 53 | { 54 | "name":"keywords", 55 | "type":"string", 56 | "value":"despair\neval\nvampirism\ntripwire\npriority\noverride\ncovariance" 57 | }, 58 | { 59 | "name":"num_keywords", 60 | "type":"int", 61 | "value":3 62 | }], 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":0, 67 | "y":768 68 | }, 69 | { 70 | "class":"", 71 | "gid":51, 72 | "height":96, 73 | "id":119, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":672, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":8, 84 | "height":96, 85 | "id":121, 86 | "name":"", 87 | "rotation":90, 88 | "visible":true, 89 | "width":96, 90 | "x":672, 91 | "y":288 92 | }, 93 | { 94 | "class":"", 95 | "gid":4, 96 | "height":96, 97 | "id":122, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":576 104 | }, 105 | { 106 | "class":"", 107 | "gid":51, 108 | "height":96, 109 | "id":124, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":384, 115 | "y":576 116 | }, 117 | { 118 | "class":"", 119 | "gid":2147483656, 120 | "height":96, 121 | "id":125, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":480, 127 | "y":384 128 | }, 129 | { 130 | "class":"", 131 | "gid":3, 132 | "height":96, 133 | "id":126, 134 | "name":"", 135 | "properties":[ 136 | { 137 | "name":"cond_kind", 138 | "type":"string", 139 | "value":"unchanged" 140 | }], 141 | "rotation":0, 142 | "visible":true, 143 | "width":96, 144 | "x":576, 145 | "y":384 146 | }, 147 | { 148 | "class":"", 149 | "gid":6, 150 | "height":96, 151 | "id":128, 152 | "name":"", 153 | "rotation":90, 154 | "visible":true, 155 | "width":96, 156 | "x":576, 157 | "y":384 158 | }, 159 | { 160 | "class":"", 161 | "gid":5, 162 | "height":96, 163 | "id":129, 164 | "name":"", 165 | "rotation":180, 166 | "visible":true, 167 | "width":96, 168 | "x":576, 169 | "y":480 170 | }, 171 | 172 | { 173 | "class":"", 174 | "gid":8, 175 | "height":96, 176 | "id":130, 177 | "name":"", 178 | "rotation":270, 179 | "visible":true, 180 | "width":96, 181 | "x":480, 182 | "y":480 183 | }, 184 | { 185 | "class":"", 186 | "gid":57, 187 | "height":96, 188 | "id":132, 189 | "name":"", 190 | "properties":[ 191 | { 192 | "name":"text", 193 | "type":"string", 194 | "value":"polygraphic ops\naffect 1+ letters" 195 | }], 196 | "rotation":0, 197 | "visible":true, 198 | "width":96, 199 | "x":864, 200 | "y":672 201 | }], 202 | "opacity":1, 203 | "type":"objectgroup", 204 | "visible":true, 205 | "x":0, 206 | "y":0 207 | }], 208 | "nextlayerid":3, 209 | "nextobjectid":133, 210 | "orientation":"orthogonal", 211 | "renderorder":"right-down", 212 | "tiledversion":"1.9.2", 213 | "tileheight":96, 214 | "tilesets":[ 215 | { 216 | "firstgid":1, 217 | "source":"..\/..\/schemas.tsj" 218 | }], 219 | "tilewidth":96, 220 | "type":"map", 221 | "version":"1.9", 222 | "width":12 223 | } -------------------------------------------------------------------------------- /src/_assets/levels/story/addsub_negation.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":480, 20 | "y":192 21 | }, 22 | { 23 | "class":"", 24 | "gid":21, 25 | "height":96, 26 | "id":116, 27 | "name":"", 28 | "properties":[ 29 | { 30 | "name":"keywords", 31 | "type":"string", 32 | "value":"area\nclone\nmiracle\nvulture\nstar\nsteam\noasis\ngist\ngust" 33 | }, 34 | { 35 | "name":"num_keywords", 36 | "type":"int", 37 | "value":2 38 | }], 39 | "rotation":0, 40 | "visible":true, 41 | "width":96, 42 | "x":0, 43 | "y":768 44 | }, 45 | { 46 | "class":"", 47 | "gid":2147483656, 48 | "height":96, 49 | "id":159, 50 | "name":"", 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":384, 55 | "y":192 56 | }, 57 | { 58 | "class":"", 59 | "gid":12, 60 | "height":96, 61 | "id":163, 62 | "name":"", 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":576, 67 | "y":288 68 | }, 69 | { 70 | "class":"", 71 | "gid":16, 72 | "height":96, 73 | "id":170, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":384, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":16, 84 | "height":96, 85 | "id":171, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":384, 91 | "y":480 92 | }, 93 | { 94 | "class":"", 95 | "gid":15, 96 | "height":96, 97 | "id":172, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":480, 103 | "y":384 104 | }, 105 | { 106 | "class":"", 107 | "gid":15, 108 | "height":96, 109 | "id":173, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":480, 115 | "y":576 116 | }, 117 | { 118 | "class":"", 119 | "gid":38, 120 | "height":96, 121 | "id":174, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":576, 127 | "y":480 128 | }, 129 | { 130 | "class":"", 131 | "gid":8, 132 | "height":96, 133 | "id":177, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":480, 139 | "y":288 140 | }, 141 | { 142 | "class":"", 143 | "gid":2147483656, 144 | "height":96, 145 | "id":178, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":384, 151 | "y":384 152 | }, 153 | { 154 | "class":"", 155 | "gid":8, 156 | "height":96, 157 | "id":179, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":480, 163 | "y":480 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":2147483656, 169 | "height":96, 170 | "id":180, 171 | "name":"", 172 | "rotation":180, 173 | "visible":true, 174 | "width":96, 175 | "x":672, 176 | "y":480 177 | }, 178 | { 179 | "class":"", 180 | "gid":5, 181 | "height":96, 182 | "id":181, 183 | "name":"", 184 | "rotation":270, 185 | "visible":true, 186 | "width":96, 187 | "x":672, 188 | "y":384 189 | }, 190 | { 191 | "class":"", 192 | "gid":57, 193 | "height":96, 194 | "id":182, 195 | "name":"", 196 | "properties":[ 197 | { 198 | "name":"text", 199 | "type":"string", 200 | "value":"I can do this!" 201 | }], 202 | "rotation":0, 203 | "visible":true, 204 | "width":96, 205 | "x":864, 206 | "y":576 207 | }], 208 | "opacity":1, 209 | "type":"objectgroup", 210 | "visible":true, 211 | "x":0, 212 | "y":0 213 | }], 214 | "nextlayerid":3, 215 | "nextobjectid":183, 216 | "orientation":"orthogonal", 217 | "renderorder":"right-down", 218 | "tiledversion":"1.9.2", 219 | "tileheight":96, 220 | "tilesets":[ 221 | { 222 | "firstgid":1, 223 | "source":"..\/..\/schemas.tsj" 224 | }], 225 | "tilewidth":96, 226 | "type":"map", 227 | "version":"1.9", 228 | "width":12 229 | } -------------------------------------------------------------------------------- /src/_assets/levels/bonus/rumble.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":192, 20 | "y":576 21 | }, 22 | { 23 | "class":"", 24 | "gid":12, 25 | "height":96, 26 | "id":115, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":768, 32 | "y":384 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"celsius\nambient\nglobal\nraptor\nparrot\ndeity" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":3 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":5, 60 | "height":96, 61 | "id":119, 62 | "name":"", 63 | "rotation":270, 64 | "visible":true, 65 | "width":96, 66 | "x":288, 67 | "y":480 68 | }, 69 | { 70 | "class":"", 71 | "gid":36, 72 | "height":96, 73 | "id":120, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":192, 79 | "y":384 80 | }, 81 | { 82 | "class":"", 83 | "gid":5, 84 | "height":96, 85 | "id":121, 86 | "name":"", 87 | "rotation":270, 88 | "visible":true, 89 | "width":96, 90 | "x":288, 91 | "y":288 92 | }, 93 | { 94 | "class":"", 95 | "gid":27, 96 | "height":96, 97 | "id":122, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":192, 103 | "y":192 104 | }, 105 | { 106 | "class":"", 107 | "gid":5, 108 | "height":96, 109 | "id":123, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":288, 115 | "y":192 116 | }, 117 | { 118 | "class":"", 119 | "gid":8, 120 | "height":96, 121 | "id":124, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":384, 127 | "y":192 128 | }, 129 | { 130 | "class":"", 131 | "gid":36, 132 | "height":96, 133 | "id":125, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":384, 139 | "y":288 140 | }, 141 | { 142 | "class":"", 143 | "gid":5, 144 | "height":96, 145 | "id":126, 146 | "name":"", 147 | "rotation":90, 148 | "visible":true, 149 | "width":96, 150 | "x":384, 151 | "y":288 152 | }, 153 | { 154 | "class":"", 155 | "gid":51, 156 | "height":96, 157 | "id":127, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":384, 163 | "y":480 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":5, 169 | "height":96, 170 | "id":129, 171 | "name":"", 172 | "rotation":0, 173 | "visible":true, 174 | "width":96, 175 | "x":480, 176 | "y":480 177 | }, 178 | { 179 | "class":"", 180 | "gid":2147483656, 181 | "height":96, 182 | "id":130, 183 | "name":"", 184 | "rotation":180, 185 | "visible":true, 186 | "width":96, 187 | "x":672, 188 | "y":384 189 | }, 190 | { 191 | "class":"", 192 | "gid":43, 193 | "height":96, 194 | "id":131, 195 | "name":"", 196 | "rotation":0, 197 | "visible":true, 198 | "width":96, 199 | "x":576, 200 | "y":384 201 | }, 202 | { 203 | "class":"", 204 | "gid":5, 205 | "height":96, 206 | "id":132, 207 | "name":"", 208 | "rotation":0, 209 | "visible":true, 210 | "width":96, 211 | "x":672, 212 | "y":384 213 | }], 214 | "opacity":1, 215 | "type":"objectgroup", 216 | "visible":true, 217 | "x":0, 218 | "y":0 219 | }], 220 | "nextlayerid":3, 221 | "nextobjectid":133, 222 | "orientation":"orthogonal", 223 | "renderorder":"right-down", 224 | "tiledversion":"1.9.2", 225 | "tileheight":96, 226 | "tilesets":[ 227 | { 228 | "firstgid":1, 229 | "source":"..\/..\/schemas.tsj" 230 | }], 231 | "tilewidth":96, 232 | "type":"map", 233 | "version":"1.9", 234 | "width":12 235 | } -------------------------------------------------------------------------------- /src/_assets/levels/bonus/spellbook.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":480, 20 | "y":96 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":90, 29 | "visible":true, 30 | "width":96, 31 | "x":480, 32 | "y":96 33 | }, 34 | { 35 | "class":"", 36 | "gid":12, 37 | "height":96, 38 | "id":115, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":480, 44 | "y":672 45 | }, 46 | { 47 | "class":"", 48 | "gid":21, 49 | "height":96, 50 | "id":116, 51 | "name":"", 52 | "properties":[ 53 | { 54 | "name":"keywords", 55 | "type":"string", 56 | "value":"prominence\nhurricane\nhydroblast\nicebolt\nblaze\n" 57 | }, 58 | { 59 | "name":"num_keywords", 60 | "type":"int", 61 | "value":2 62 | }], 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":0, 67 | "y":768 68 | }, 69 | { 70 | "class":"", 71 | "gid":28, 72 | "height":96, 73 | "id":117, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":480, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":41, 84 | "height":96, 85 | "id":118, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":480, 91 | "y":480 92 | }, 93 | { 94 | "class":"", 95 | "gid":7, 96 | "height":96, 97 | "id":119, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":576, 103 | "y":288 104 | }, 105 | { 106 | "class":"", 107 | "gid":22, 108 | "height":96, 109 | "id":120, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":576, 115 | "y":384 116 | }, 117 | { 118 | "class":"", 119 | "gid":9, 120 | "height":96, 121 | "id":122, 122 | "name":"", 123 | "rotation":90, 124 | "visible":true, 125 | "width":96, 126 | "x":480, 127 | "y":288 128 | }, 129 | { 130 | "class":"", 131 | "gid":42, 132 | "height":96, 133 | "id":123, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":384, 139 | "y":384 140 | }, 141 | { 142 | "class":"", 143 | "gid":2147483656, 144 | "height":96, 145 | "id":124, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":384, 151 | "y":288 152 | }, 153 | { 154 | "class":"", 155 | "gid":7, 156 | "height":96, 157 | "id":125, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":576, 163 | "y":480 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":2147483656, 169 | "height":96, 170 | "id":126, 171 | "name":"", 172 | "rotation":0, 173 | "visible":true, 174 | "width":96, 175 | "x":384, 176 | "y":480 177 | }, 178 | { 179 | "class":"", 180 | "gid":42, 181 | "height":96, 182 | "id":127, 183 | "name":"", 184 | "rotation":0, 185 | "visible":true, 186 | "width":96, 187 | "x":384, 188 | "y":576 189 | }, 190 | { 191 | "class":"", 192 | "gid":9, 193 | "height":96, 194 | "id":129, 195 | "name":"", 196 | "rotation":90, 197 | "visible":true, 198 | "width":96, 199 | "x":480, 200 | "y":480 201 | }, 202 | { 203 | "class":"", 204 | "gid":20, 205 | "height":96, 206 | "id":130, 207 | "name":"", 208 | "rotation":0, 209 | "visible":true, 210 | "width":96, 211 | "x":576, 212 | "y":576 213 | }], 214 | "opacity":1, 215 | "type":"objectgroup", 216 | "visible":true, 217 | "x":0, 218 | "y":0 219 | }], 220 | "nextlayerid":3, 221 | "nextobjectid":131, 222 | "orientation":"orthogonal", 223 | "renderorder":"right-down", 224 | "tiledversion":"1.9.2", 225 | "tileheight":96, 226 | "tilesets":[ 227 | { 228 | "firstgid":1, 229 | "source":"..\/..\/schemas.tsj" 230 | }], 231 | "tilewidth":96, 232 | "type":"map", 233 | "version":"1.9", 234 | "width":12 235 | } -------------------------------------------------------------------------------- /src/_assets/levels/bonus/pyramid.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":576, 20 | "y":576 21 | }, 22 | { 23 | "class":"", 24 | "gid":12, 25 | "height":96, 26 | "id":115, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":480, 32 | "y":480 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"urban\nballot\nbandwidth\nforge\ncontrail" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":3 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":8, 60 | "height":96, 61 | "id":121, 62 | "name":"", 63 | "rotation":270, 64 | "visible":true, 65 | "width":96, 66 | "x":672, 67 | "y":480 68 | }, 69 | { 70 | "class":"", 71 | "gid":2147483656, 72 | "height":96, 73 | "id":122, 74 | "name":"", 75 | "rotation":90, 76 | "visible":true, 77 | "width":96, 78 | "x":672, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":2147483656, 84 | "height":96, 85 | "id":131, 86 | "name":"", 87 | "rotation":0, 88 | "visible":true, 89 | "width":96, 90 | "x":384, 91 | "y":288 92 | }, 93 | { 94 | "class":"", 95 | "gid":47, 96 | "height":96, 97 | "id":134, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":672, 103 | "y":480 104 | }, 105 | { 106 | "class":"", 107 | "gid":2147483656, 108 | "height":96, 109 | "id":136, 110 | "name":"", 111 | "rotation":90, 112 | "visible":true, 113 | "width":96, 114 | "x":576, 115 | "y":192 116 | }, 117 | { 118 | "class":"", 119 | "gid":47, 120 | "height":96, 121 | "id":137, 122 | "name":"", 123 | "rotation":0, 124 | "visible":true, 125 | "width":96, 126 | "x":480, 127 | "y":288 128 | }, 129 | { 130 | "class":"", 131 | "gid":14, 132 | "height":96, 133 | "id":141, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":576, 139 | "y":384 140 | }, 141 | { 142 | "class":"", 143 | "gid":13, 144 | "height":96, 145 | "id":142, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":288, 151 | "y":480 152 | }, 153 | { 154 | "class":"", 155 | "gid":2147483656, 156 | "height":96, 157 | "id":143, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":288, 163 | "y":384 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":30, 169 | "height":96, 170 | "id":144, 171 | "name":"", 172 | "rotation":0, 173 | "visible":true, 174 | "width":96, 175 | "x":384, 176 | "y":384 177 | }, 178 | { 179 | "class":"", 180 | "gid":8, 181 | "height":96, 182 | "id":145, 183 | "name":"", 184 | "rotation":0, 185 | "visible":true, 186 | "width":96, 187 | "x":384, 188 | "y":480 189 | }, 190 | { 191 | "class":"", 192 | "gid":44, 193 | "height":96, 194 | "id":147, 195 | "name":"", 196 | "rotation":0, 197 | "visible":true, 198 | "width":96, 199 | "x":384, 200 | "y":576 201 | }, 202 | { 203 | "class":"", 204 | "gid":2147483656, 205 | "height":96, 206 | "id":148, 207 | "name":"", 208 | "rotation":180, 209 | "visible":true, 210 | "width":96, 211 | "x":576, 212 | "y":480 213 | }], 214 | "opacity":1, 215 | "type":"objectgroup", 216 | "visible":true, 217 | "x":0, 218 | "y":0 219 | }], 220 | "nextlayerid":3, 221 | "nextobjectid":149, 222 | "orientation":"orthogonal", 223 | "renderorder":"right-down", 224 | "tiledversion":"1.9.2", 225 | "tileheight":96, 226 | "tilesets":[ 227 | { 228 | "firstgid":1, 229 | "source":"..\/..\/schemas.tsj" 230 | }], 231 | "tilewidth":96, 232 | "type":"map", 233 | "version":"1.9", 234 | "width":12 235 | } -------------------------------------------------------------------------------- /src/_assets/levels/bonus/clear_head.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":288, 20 | "y":384 21 | }, 22 | { 23 | "class":"", 24 | "gid":5, 25 | "height":96, 26 | "id":77, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":384, 32 | "y":192 33 | }, 34 | { 35 | "class":"", 36 | "gid":5, 37 | "height":96, 38 | "id":114, 39 | "name":"", 40 | "rotation":0, 41 | "visible":true, 42 | "width":96, 43 | "x":576, 44 | "y":576 45 | }, 46 | { 47 | "class":"", 48 | "gid":12, 49 | "height":96, 50 | "id":115, 51 | "name":"", 52 | "rotation":0, 53 | "visible":true, 54 | "width":96, 55 | "x":672, 56 | "y":576 57 | }, 58 | { 59 | "class":"", 60 | "gid":21, 61 | "height":96, 62 | "id":116, 63 | "name":"", 64 | "properties":[ 65 | { 66 | "name":"keywords", 67 | "type":"string", 68 | "value":"olive\nlinux\ncursor\nobelisk\nstream\nvertex" 69 | }, 70 | { 71 | "name":"num_keywords", 72 | "type":"int", 73 | "value":2 74 | }], 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":0, 79 | "y":768 80 | }, 81 | { 82 | "class":"", 83 | "gid":5, 84 | "height":96, 85 | "id":119, 86 | "name":"", 87 | "rotation":90, 88 | "visible":true, 89 | "width":96, 90 | "x":480, 91 | "y":192 92 | }, 93 | { 94 | "class":"", 95 | "gid":3, 96 | "height":96, 97 | "id":120, 98 | "name":"", 99 | "properties":[ 100 | { 101 | "name":"cond_kind", 102 | "type":"string", 103 | "value":"last_gt" 104 | }, 105 | { 106 | "name":"string_arg", 107 | "type":"string", 108 | "value":"m" 109 | }], 110 | "rotation":0, 111 | "visible":true, 112 | "width":96, 113 | "x":480, 114 | "y":384 115 | }, 116 | { 117 | "class":"", 118 | "gid":6, 119 | "height":96, 120 | "id":123, 121 | "name":"", 122 | "rotation":0, 123 | "visible":true, 124 | "width":96, 125 | "x":576, 126 | "y":384 127 | }, 128 | { 129 | "class":"", 130 | "gid":46, 131 | "height":96, 132 | "id":125, 133 | "name":"", 134 | "rotation":0, 135 | "visible":true, 136 | "width":96, 137 | "x":480, 138 | "y":576 139 | }, 140 | { 141 | "class":"", 142 | "gid":5, 143 | "height":96, 144 | "id":126, 145 | "name":"", 146 | "rotation":90, 147 | "visible":true, 148 | "width":96, 149 | "x":480, 150 | "y":384 151 | }, 152 | { 153 | "class":"", 154 | "gid":5, 155 | "height":96, 156 | "id":127, 157 | "name":"", 158 | "rotation":90, 159 | "visible":true, 160 | "width":96, 161 | "x":672, 162 | "y":384 163 | }, 164 | { 165 | "class":"", 166 | "gid":39, 167 | "height":96, 168 | "id":128, 169 | "name":"", 170 | "rotation":0, 171 | "visible":true, 172 | "width":96, 173 | "x":672, 174 | "y":384 175 | }, 176 | 177 | { 178 | "class":"", 179 | "gid":55, 180 | "height":96, 181 | "id":129, 182 | "name":"", 183 | "rotation":0, 184 | "visible":true, 185 | "width":96, 186 | "x":480, 187 | "y":192 188 | }, 189 | { 190 | "class":"", 191 | "gid":46, 192 | "height":96, 193 | "id":130, 194 | "name":"", 195 | "rotation":0, 196 | "visible":true, 197 | "width":96, 198 | "x":288, 199 | "y":192 200 | }, 201 | { 202 | "class":"", 203 | "gid":5, 204 | "height":96, 205 | "id":131, 206 | "name":"", 207 | "rotation":270, 208 | "visible":true, 209 | "width":96, 210 | "x":384, 211 | "y":288 212 | }], 213 | "opacity":1, 214 | "type":"objectgroup", 215 | "visible":true, 216 | "x":0, 217 | "y":0 218 | }], 219 | "nextlayerid":3, 220 | "nextobjectid":132, 221 | "orientation":"orthogonal", 222 | "renderorder":"right-down", 223 | "tiledversion":"1.9.2", 224 | "tileheight":96, 225 | "tilesets":[ 226 | { 227 | "firstgid":1, 228 | "source":"..\/..\/schemas.tsj" 229 | }], 230 | "tilewidth":96, 231 | "type":"map", 232 | "version":"1.9", 233 | "width":12 234 | } -------------------------------------------------------------------------------- /src/_assets/levels/story/atbash.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":288, 20 | "y":288 21 | }, 22 | { 23 | "class":"", 24 | "gid":21, 25 | "height":96, 26 | "id":116, 27 | "name":"", 28 | "properties":[ 29 | { 30 | "name":"keywords", 31 | "type":"string", 32 | "value":"barbarian\nbeetle\nturtle\nsnake\nmoon\npanda" 33 | }, 34 | { 35 | "name":"num_keywords", 36 | "type":"int", 37 | "value":2 38 | }], 39 | "rotation":0, 40 | "visible":true, 41 | "width":96, 42 | "x":0, 43 | "y":768 44 | }, 45 | { 46 | "class":"", 47 | "gid":2147483656, 48 | "height":96, 49 | "id":171, 50 | "name":"", 51 | "rotation":270, 52 | "visible":true, 53 | "width":96, 54 | "x":384, 55 | "y":384 56 | }, 57 | { 58 | "class":"", 59 | "gid":24, 60 | "height":96, 61 | "id":173, 62 | "name":"", 63 | "properties":[ 64 | { 65 | "name":"cond_kind", 66 | "type":"string", 67 | "value":"contains_letter" 68 | }, 69 | { 70 | "name":"string_arg", 71 | "type":"string", 72 | "value":"b" 73 | }], 74 | "rotation":0, 75 | "visible":true, 76 | "width":96, 77 | "x":384, 78 | "y":384 79 | }, 80 | { 81 | "class":"", 82 | "gid":6, 83 | "height":96, 84 | "id":174, 85 | "name":"", 86 | "rotation":0, 87 | "visible":true, 88 | "width":96, 89 | "x":480, 90 | "y":384 91 | }, 92 | { 93 | "class":"", 94 | "gid":5, 95 | "height":96, 96 | "id":176, 97 | "name":"", 98 | "rotation":0, 99 | "visible":true, 100 | "width":96, 101 | "x":672, 102 | "y":384 103 | }, 104 | { 105 | "class":"", 106 | "gid":12, 107 | "height":96, 108 | "id":177, 109 | "name":"", 110 | "rotation":0, 111 | "visible":true, 112 | "width":96, 113 | "x":768, 114 | "y":384 115 | }, 116 | { 117 | "class":"", 118 | "gid":5, 119 | "height":96, 120 | "id":178, 121 | "name":"", 122 | "rotation":90, 123 | "visible":true, 124 | "width":96, 125 | "x":384, 126 | "y":384 127 | }, 128 | { 129 | "class":"", 130 | "gid":23, 131 | "height":96, 132 | "id":179, 133 | "name":"", 134 | "rotation":0, 135 | "visible":true, 136 | "width":96, 137 | "x":384, 138 | "y":576 139 | }, 140 | { 141 | "class":"", 142 | "gid":5, 143 | "height":96, 144 | "id":180, 145 | "name":"", 146 | "rotation":0, 147 | "visible":true, 148 | "width":96, 149 | "x":480, 150 | "y":576 151 | }, 152 | { 153 | "class":"", 154 | "gid":13, 155 | "height":96, 156 | "id":181, 157 | "name":"", 158 | "rotation":0, 159 | "visible":true, 160 | "width":96, 161 | "x":576, 162 | "y":576 163 | }, 164 | { 165 | "class":"", 166 | "gid":5, 167 | "height":96, 168 | "id":182, 169 | "name":"", 170 | "rotation":0, 171 | "visible":true, 172 | "width":96, 173 | "x":672, 174 | "y":576 175 | }, 176 | 177 | { 178 | "class":"", 179 | "gid":2147483656, 180 | "height":96, 181 | "id":183, 182 | "name":"", 183 | "rotation":180, 184 | "visible":true, 185 | "width":96, 186 | "x":864, 187 | "y":480 188 | }, 189 | { 190 | "class":"", 191 | "gid":5, 192 | "height":96, 193 | "id":184, 194 | "name":"", 195 | "rotation":270, 196 | "visible":true, 197 | "width":96, 198 | "x":864, 199 | "y":480 200 | }, 201 | { 202 | "class":"", 203 | "gid":26, 204 | "height":96, 205 | "id":185, 206 | "name":"", 207 | "rotation":0, 208 | "visible":true, 209 | "width":96, 210 | "x":576, 211 | "y":384 212 | }], 213 | "opacity":1, 214 | "type":"objectgroup", 215 | "visible":true, 216 | "x":0, 217 | "y":0 218 | }], 219 | "nextlayerid":3, 220 | "nextobjectid":187, 221 | "orientation":"orthogonal", 222 | "renderorder":"right-down", 223 | "tiledversion":"1.9.2", 224 | "tileheight":96, 225 | "tilesets":[ 226 | { 227 | "firstgid":1, 228 | "source":"..\/..\/schemas.tsj" 229 | }], 230 | "tilewidth":96, 231 | "type":"map", 232 | "version":"1.9", 233 | "width":12 234 | } -------------------------------------------------------------------------------- /src/_assets/levels/story/red_herring.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":8, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "draworder":"topdown", 7 | "id":2, 8 | "name":"scheme", 9 | "objects":[ 10 | { 11 | "class":"", 12 | "gid":11, 13 | "height":96, 14 | "id":51, 15 | "name":"", 16 | "rotation":0, 17 | "visible":true, 18 | "width":96, 19 | "x":192, 20 | "y":288 21 | }, 22 | { 23 | "class":"", 24 | "gid":12, 25 | "height":96, 26 | "id":115, 27 | "name":"", 28 | "rotation":0, 29 | "visible":true, 30 | "width":96, 31 | "x":576, 32 | "y":384 33 | }, 34 | { 35 | "class":"", 36 | "gid":21, 37 | "height":96, 38 | "id":116, 39 | "name":"", 40 | "properties":[ 41 | { 42 | "name":"keywords", 43 | "type":"string", 44 | "value":"arab\nprob\nprimero\ncold" 45 | }, 46 | { 47 | "name":"num_keywords", 48 | "type":"int", 49 | "value":3 50 | }], 51 | "rotation":0, 52 | "visible":true, 53 | "width":96, 54 | "x":0, 55 | "y":768 56 | }, 57 | { 58 | "class":"", 59 | "gid":5, 60 | "height":96, 61 | "id":117, 62 | "name":"", 63 | "rotation":0, 64 | "visible":true, 65 | "width":96, 66 | "x":288, 67 | "y":288 68 | }, 69 | { 70 | "class":"", 71 | "gid":33, 72 | "height":96, 73 | "id":118, 74 | "name":"", 75 | "rotation":0, 76 | "visible":true, 77 | "width":96, 78 | "x":384, 79 | "y":288 80 | }, 81 | { 82 | "class":"", 83 | "gid":2147483655, 84 | "height":96, 85 | "id":120, 86 | "name":"", 87 | "rotation":180, 88 | "visible":true, 89 | "width":96, 90 | "x":576, 91 | "y":192 92 | }, 93 | { 94 | "class":"", 95 | "gid":29, 96 | "height":96, 97 | "id":121, 98 | "name":"", 99 | "rotation":0, 100 | "visible":true, 101 | "width":96, 102 | "x":480, 103 | "y":192 104 | }, 105 | { 106 | "class":"", 107 | "gid":2147483656, 108 | "height":96, 109 | "id":122, 110 | "name":"", 111 | "rotation":0, 112 | "visible":true, 113 | "width":96, 114 | "x":384, 115 | "y":192 116 | }, 117 | { 118 | "class":"", 119 | "gid":5, 120 | "height":96, 121 | "id":123, 122 | "name":"", 123 | "rotation":90, 124 | "visible":true, 125 | "width":96, 126 | "x":384, 127 | "y":384 128 | }, 129 | { 130 | "class":"", 131 | "gid":27, 132 | "height":96, 133 | "id":124, 134 | "name":"", 135 | "rotation":0, 136 | "visible":true, 137 | "width":96, 138 | "x":384, 139 | "y":576 140 | }, 141 | { 142 | "class":"", 143 | "gid":33, 144 | "height":96, 145 | "id":126, 146 | "name":"", 147 | "rotation":0, 148 | "visible":true, 149 | "width":96, 150 | "x":576, 151 | "y":576 152 | }, 153 | { 154 | "class":"", 155 | "gid":5, 156 | "height":96, 157 | "id":127, 158 | "name":"", 159 | "rotation":0, 160 | "visible":true, 161 | "width":96, 162 | "x":480, 163 | "y":576 164 | }, 165 | 166 | { 167 | "class":"", 168 | "gid":5, 169 | "height":96, 170 | "id":128, 171 | "name":"", 172 | "rotation":270, 173 | "visible":true, 174 | "width":96, 175 | "x":672, 176 | "y":480 177 | }, 178 | { 179 | "class":"", 180 | "gid":5, 181 | "height":96, 182 | "id":129, 183 | "name":"", 184 | "rotation":90, 185 | "visible":true, 186 | "width":96, 187 | "x":384, 188 | "y":288 189 | }, 190 | { 191 | "class":"", 192 | "gid":7, 193 | "height":96, 194 | "id":130, 195 | "name":"", 196 | "rotation":0, 197 | "visible":true, 198 | "width":96, 199 | "x":672, 200 | "y":576 201 | }, 202 | { 203 | "class":"", 204 | "gid":8, 205 | "height":96, 206 | "id":131, 207 | "name":"", 208 | "rotation":180, 209 | "visible":true, 210 | "width":96, 211 | "x":672, 212 | "y":576 213 | }, 214 | { 215 | "class":"", 216 | "gid":15, 217 | "height":96, 218 | "id":132, 219 | "name":"", 220 | "rotation":0, 221 | "visible":true, 222 | "width":96, 223 | "x":672, 224 | "y":672 225 | }], 226 | "opacity":1, 227 | "type":"objectgroup", 228 | "visible":true, 229 | "x":0, 230 | "y":0 231 | }], 232 | "nextlayerid":3, 233 | "nextobjectid":133, 234 | "orientation":"orthogonal", 235 | "renderorder":"right-down", 236 | "tiledversion":"1.9.2", 237 | "tileheight":96, 238 | "tilesets":[ 239 | { 240 | "firstgid":1, 241 | "source":"..\/..\/schemas.tsj" 242 | }], 243 | "tilewidth":96, 244 | "type":"map", 245 | "version":"1.9", 246 | "width":12 247 | } --------------------------------------------------------------------------------