├── .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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------