├── .gitignore ├── .parcelrc ├── .vscode └── settings.json ├── README.md ├── package-lock.json ├── package.json ├── src ├── .postcssrc ├── @types │ └── has │ │ └── index.d.ts ├── LICENSE ├── assets │ ├── audio │ │ ├── alien_attack_1.webm │ │ ├── alien_attack_2.webm │ │ ├── alien_attack_3.webm │ │ ├── alien_attack_4.webm │ │ ├── alien_death_1.webm │ │ ├── alien_death_2.webm │ │ ├── alien_death_3.webm │ │ ├── alien_move_1.webm │ │ ├── alien_move_2.webm │ │ ├── alien_move_3.webm │ │ ├── alien_move_4.webm │ │ ├── building_built.webm │ │ ├── building_destroyed.webm │ │ ├── buzz intro.wav │ │ ├── buzz.webm │ │ ├── colonist_move_1.webm │ │ ├── colonist_move_2.webm │ │ ├── colonist_move_3.webm │ │ ├── colonist_move_4.webm │ │ ├── day_intro.webm │ │ ├── day_loop.webm │ │ ├── explosion_1.webm │ │ ├── explosion_2.webm │ │ ├── explosion_3.webm │ │ ├── explosion_4.webm │ │ ├── explosion_5.webm │ │ ├── explosion_6.webm │ │ ├── explosion_7.webm │ │ ├── factory.webm │ │ ├── factory_1.webm │ │ ├── factory_2.webm │ │ ├── farm.webm │ │ ├── laser_activate.webm │ │ ├── laser_active.webm │ │ ├── laser_cancel.webm │ │ ├── laser_shot_1.webm │ │ ├── laser_shot_2.webm │ │ ├── laser_shot_3.webm │ │ ├── laser_shot_4.webm │ │ ├── laser_shot_5.webm │ │ ├── laser_shot_6.webm │ │ ├── mine_1.webm │ │ ├── mine_2.webm │ │ ├── night_intro.webm │ │ ├── night_loop.webm │ │ ├── pickaxe_loop.webm │ │ ├── player_move_1.webm │ │ ├── player_move_2.webm │ │ ├── player_move_3.webm │ │ ├── player_move_4.webm │ │ ├── power_off.webm │ │ ├── power_on.webm │ │ ├── ui_alert.webm │ │ ├── ui_chime.webm │ │ ├── ui_successful_valid.webm │ │ └── ui_unsuccessful_invalid.webm │ ├── style.css │ └── tiles │ │ ├── absorber.png │ │ ├── absorber_charge.png │ │ ├── basic_projector.png │ │ ├── battery.png │ │ ├── blank.png │ │ ├── border.png │ │ ├── colonists1.png │ │ ├── colonists2.png │ │ ├── colonists3.png │ │ ├── cursor_build.png │ │ ├── cursor_cancel.png │ │ ├── cursor_move.png │ │ ├── cursor_reflector.png │ │ ├── direction-indicator.png │ │ ├── disabled.png │ │ ├── enemy_armored_1.png │ │ ├── enemy_armored_2.png │ │ ├── enemy_burrowed_1.png │ │ ├── enemy_burrowed_2.png │ │ ├── enemy_burrower_1.png │ │ ├── enemy_burrower_2.png │ │ ├── enemy_drone_1.png │ │ ├── enemy_drone_2.png │ │ ├── enemy_flyer_1.png │ │ ├── enemy_flyer_2.png │ │ ├── enemy_volatile_1.png │ │ ├── enemy_volatile_2.png │ │ ├── exclamation_double.png │ │ ├── exclamation_single.png │ │ ├── exclamation_triple.png │ │ ├── factory.png │ │ ├── farm.png │ │ ├── farm_growth_1.png │ │ ├── farm_growth_2.png │ │ ├── farm_growth_3.png │ │ ├── fertile.png │ │ ├── floor.png │ │ ├── food.png │ │ ├── laser.png │ │ ├── laser_4split.png │ │ ├── laser_burst.png │ │ ├── laser_player_down.png │ │ ├── laser_player_left.png │ │ ├── laser_player_right.png │ │ ├── laser_player_up.png │ │ ├── laser_reflected.png │ │ ├── laser_split.png │ │ ├── machinery.png │ │ ├── metal.png │ │ ├── mine.png │ │ ├── mining_spot.png │ │ ├── mountain.png │ │ ├── ore.png │ │ ├── outline_dashed.png │ │ ├── outline_exclamation.png │ │ ├── outline_solid.png │ │ ├── player.png │ │ ├── power.png │ │ ├── powerplant.png │ │ ├── projector.png │ │ ├── reflector.png │ │ ├── residence.png │ │ ├── road_0.png │ │ ├── road_1.png │ │ ├── road_10.png │ │ ├── road_11.png │ │ ├── road_12.png │ │ ├── road_13.png │ │ ├── road_14.png │ │ ├── road_15.png │ │ ├── road_2.png │ │ ├── road_3.png │ │ ├── road_4.png │ │ ├── road_5.png │ │ ├── road_6.png │ │ ├── road_7.png │ │ ├── road_8.png │ │ ├── road_9.png │ │ ├── rubble.png │ │ ├── shield.xcf │ │ ├── shield_1.png │ │ ├── shield_2.png │ │ ├── shield_3.png │ │ ├── shield_generator.png │ │ ├── skull.png │ │ ├── solarpanel.png │ │ ├── splitter.png │ │ ├── splitter_advanced.png │ │ ├── target.png │ │ ├── tent.png │ │ ├── wall.png │ │ ├── wall_crumbling.png │ │ ├── wall_damaged.png │ │ ├── warehouse.png │ │ ├── water_0.png │ │ ├── water_15.png │ │ ├── water_4.png │ │ ├── water_5.png │ │ ├── water_6.png │ │ ├── water_7.png │ │ ├── water_corner.png │ │ ├── windmill-1.png │ │ ├── windmill-2.png │ │ ├── windmill-3.png │ │ ├── windmill-4.png │ │ ├── windows_factory_1.png │ │ ├── windows_factory_2.png │ │ ├── windows_mine_1.png │ │ ├── windows_mine_2.png │ │ ├── windows_residence_1.png │ │ ├── windows_residence_2.png │ │ ├── windows_residence_3.png │ │ ├── windows_tent_1.png │ │ ├── windows_warehouse_1.png │ │ └── windows_warehouse_2.png ├── colors.ts ├── constants.ts ├── data │ ├── buildingCategories.ts │ ├── colonistStatuses.ts │ ├── colors.json │ ├── defaultSettings.ts │ ├── effects.ts │ ├── jobTypes.ts │ ├── mapTypes.ts │ ├── resources.ts │ ├── templates │ │ ├── blueprints.ts │ │ ├── buildings.ts │ │ ├── enemies.ts │ │ ├── index.ts │ │ ├── lasers.ts │ │ ├── misc.ts │ │ └── terrain.ts │ └── tutorials.ts ├── hooks.ts ├── index.html ├── index.tsx ├── lib │ ├── ai.ts │ ├── audio │ │ ├── Audio.ts │ │ ├── DummyAudio.ts │ │ └── index.ts │ ├── building.ts │ ├── conditions.ts │ ├── controls.ts │ ├── entities.ts │ ├── gameSave.ts │ ├── generateMap.ts │ ├── geometry.ts │ ├── lasers.ts │ ├── makeLevel.ts │ ├── math.ts │ ├── notifications.ts │ ├── rng.ts │ └── tutorials.ts ├── messages.ts ├── renderer │ ├── Renderer.ts │ └── index.ts ├── state │ ├── actions │ │ ├── addEntity.ts │ │ ├── autoMove.ts │ │ ├── blueprintBuild.ts │ │ ├── blueprintCancel.ts │ │ ├── blueprintMove.ts │ │ ├── blueprintSelect.ts │ │ ├── bordersDraw.ts │ │ ├── bordersRemove.ts │ │ ├── bordersUpdate.ts │ │ ├── cancelAutoMove.ts │ │ ├── clearReflectors.ts │ │ ├── completeTutorial.ts │ │ ├── completeTutorialStep.ts │ │ ├── continueVictory.ts │ │ ├── cycleReflector.ts │ │ ├── deactivateWeapon.ts │ │ ├── decreaseJobPriority.ts │ │ ├── destroy.ts │ │ ├── destroyPos.ts │ │ ├── executeRemoveBuilding.ts │ │ ├── farmGrowthUpdateTile.ts │ │ ├── fireWeapon.ts │ │ ├── increaseJobPriority.ts │ │ ├── index.ts │ │ ├── loadGame.ts │ │ ├── logEvent.ts │ │ ├── logMessage.ts │ │ ├── makeMeRich.ts │ │ ├── modifyResource.ts │ │ ├── move.ts │ │ ├── newGame.ts │ │ ├── playerTookTurn.ts │ │ ├── rebuild.ts │ │ ├── reduceMorale.ts │ │ ├── removeEntities.ts │ │ ├── removeEntity.ts │ │ ├── removeReflector.ts │ │ ├── resetTutorials.ts │ │ ├── roadUpdateTile.ts │ │ ├── rotateEntity.ts │ │ ├── setAutoMovePath.ts │ │ ├── setJobPriority.ts │ │ ├── shieldCharge.ts │ │ ├── shieldDischarge.ts │ │ ├── startTutorial.ts │ │ ├── targetWeapon.ts │ │ ├── temperatureDecrease.ts │ │ ├── temperatureIncrease.ts │ │ ├── toggleDisabled.ts │ │ ├── undoTurn.ts │ │ └── updateEntity.ts │ ├── handleAction.ts │ ├── initialState.ts │ ├── reducer.ts │ ├── selectors │ │ ├── blueprintSelectors.ts │ │ ├── entitySelectors.ts │ │ ├── index.ts │ │ ├── miscSelectors.ts │ │ ├── statusSelectors.ts │ │ └── tutorialSelectors.ts │ ├── store.ts │ ├── systems │ │ ├── absorberSystem.ts │ │ ├── absorberTriggerSystem.ts │ │ ├── aiSystem.ts │ │ ├── aimingSystem.ts │ │ ├── animationToggleSystem.ts │ │ ├── audioToggleSystem.ts │ │ ├── bordersSystem.ts │ │ ├── buildingSystem.ts │ │ ├── colonistsSystem.ts │ │ ├── colorToggleSystem.ts │ │ ├── directionIndicationSystem.ts │ │ ├── emitterSystem.ts │ │ ├── eventSystem.ts │ │ ├── gameOverSystem.ts │ │ ├── hungerSystem.ts │ │ ├── immigrationSystem.ts │ │ ├── index.ts │ │ ├── laserRechargingSystem.ts │ │ ├── missingResourceIndicatorSystem.ts │ │ ├── poweredSystem.ts │ │ ├── productionSystem.ts │ │ ├── reflectorSystem.ts │ │ ├── shieldSystem.ts │ │ ├── storageSystem.ts │ │ ├── timeSystem.ts │ │ ├── waveSystem.ts │ │ └── windowsSystem.ts │ └── wrapState.ts ├── types │ ├── Action.ts │ ├── ConditionName.ts │ ├── ControlCode.ts │ ├── Direction.ts │ ├── Effect.ts │ ├── Entity.ts │ ├── RawState.ts │ ├── Settings.ts │ ├── TemplateName.ts │ ├── Tutorial.ts │ ├── TutorialId.ts │ ├── WrappedState.ts │ └── index.ts └── ui │ ├── BottomMenu.tsx │ ├── ContextMenu.tsx │ ├── CursorProvider.tsx │ ├── Demo.tsx │ ├── EntityPreview.tsx │ ├── GameMap.tsx │ ├── GameOver.tsx │ ├── GameProvider.tsx │ ├── Header.tsx │ ├── HotkeyButton.tsx │ ├── HotkeysProvider.tsx │ ├── Icons.tsx │ ├── Inspector.tsx │ ├── Introduction.tsx │ ├── Jobs.tsx │ ├── Kbd.tsx │ ├── Laser.tsx │ ├── LazyTippy.tsx │ ├── MainTitle.tsx │ ├── MapTooltip.tsx │ ├── Menu.tsx │ ├── MenuButton.tsx │ ├── MenuOptionSelector.tsx │ ├── MenuSectionHeader.tsx │ ├── MenuSlider.tsx │ ├── MenuTitle.tsx │ ├── Modal.tsx │ ├── ResourceAmount.tsx │ ├── ResourceIcon.tsx │ ├── Resources.tsx │ ├── Router.tsx │ ├── SettingsProvider.tsx │ ├── Status.tsx │ ├── Tutorials.tsx │ ├── Warning.tsx │ └── pages │ ├── Credits.tsx │ ├── Game.tsx │ ├── Keybindings.tsx │ ├── MainMenu.tsx │ ├── NewGame.tsx │ ├── Settings.tsx │ └── index.ts ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .parcel-cache 3 | node_modules 4 | dist 5 | *.zip -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "resolvers": [ 4 | "@parcel/resolver-glob", 5 | "..." 6 | ], 7 | "transformers": { 8 | "*.webm": [ 9 | "@parcel/transformer-raw" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "disablers", 5 | "gifshot", 6 | "keyval", 7 | "notyf", 8 | "pixi" 9 | ], 10 | "typescript.tsdk": "node_modules/typescript/lib" 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reflector: Laser Defense 2 | 3 | Reflector: Laser Defense is a hybrid roguelike base-builder game. For more information about the game, check out the main page at https://mscottmoore.itch.io/reflector or the devlog and https://mscottmoore.dev. 4 | 5 | ## Running the Game 6 | 7 | Assuming you have npm installed on your system, go to the root directory of this project then run `npm i` and `npm start`. Then the game will be available at `localhost:1234`. 8 | 9 | ## License 10 | 11 | The code is licensed under a GPL license. The art assets are all public domain, either created by me or public domain assets from https://kenney.nl. 12 | -------------------------------------------------------------------------------- /src/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": {}, 4 | "tailwindcss": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/@types/has/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "has" { 2 | function has(object: any, property: string): boolean; 3 | export = has; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/audio/alien_attack_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_attack_1.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_attack_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_attack_2.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_attack_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_attack_3.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_attack_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_attack_4.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_death_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_death_1.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_death_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_death_2.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_death_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_death_3.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_move_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_move_1.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_move_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_move_2.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_move_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_move_3.webm -------------------------------------------------------------------------------- /src/assets/audio/alien_move_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/alien_move_4.webm -------------------------------------------------------------------------------- /src/assets/audio/building_built.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/building_built.webm -------------------------------------------------------------------------------- /src/assets/audio/building_destroyed.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/building_destroyed.webm -------------------------------------------------------------------------------- /src/assets/audio/buzz intro.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/buzz intro.wav -------------------------------------------------------------------------------- /src/assets/audio/buzz.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/buzz.webm -------------------------------------------------------------------------------- /src/assets/audio/colonist_move_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/colonist_move_1.webm -------------------------------------------------------------------------------- /src/assets/audio/colonist_move_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/colonist_move_2.webm -------------------------------------------------------------------------------- /src/assets/audio/colonist_move_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/colonist_move_3.webm -------------------------------------------------------------------------------- /src/assets/audio/colonist_move_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/colonist_move_4.webm -------------------------------------------------------------------------------- /src/assets/audio/day_intro.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/day_intro.webm -------------------------------------------------------------------------------- /src/assets/audio/day_loop.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/day_loop.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_1.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_2.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_3.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_4.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_5.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_5.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_6.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_6.webm -------------------------------------------------------------------------------- /src/assets/audio/explosion_7.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/explosion_7.webm -------------------------------------------------------------------------------- /src/assets/audio/factory.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/factory.webm -------------------------------------------------------------------------------- /src/assets/audio/factory_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/factory_1.webm -------------------------------------------------------------------------------- /src/assets/audio/factory_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/factory_2.webm -------------------------------------------------------------------------------- /src/assets/audio/farm.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/farm.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_activate.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_activate.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_active.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_active.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_cancel.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_cancel.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_1.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_2.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_3.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_4.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_5.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_5.webm -------------------------------------------------------------------------------- /src/assets/audio/laser_shot_6.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/laser_shot_6.webm -------------------------------------------------------------------------------- /src/assets/audio/mine_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/mine_1.webm -------------------------------------------------------------------------------- /src/assets/audio/mine_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/mine_2.webm -------------------------------------------------------------------------------- /src/assets/audio/night_intro.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/night_intro.webm -------------------------------------------------------------------------------- /src/assets/audio/night_loop.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/night_loop.webm -------------------------------------------------------------------------------- /src/assets/audio/pickaxe_loop.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/pickaxe_loop.webm -------------------------------------------------------------------------------- /src/assets/audio/player_move_1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/player_move_1.webm -------------------------------------------------------------------------------- /src/assets/audio/player_move_2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/player_move_2.webm -------------------------------------------------------------------------------- /src/assets/audio/player_move_3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/player_move_3.webm -------------------------------------------------------------------------------- /src/assets/audio/player_move_4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/player_move_4.webm -------------------------------------------------------------------------------- /src/assets/audio/power_off.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/power_off.webm -------------------------------------------------------------------------------- /src/assets/audio/power_on.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/power_on.webm -------------------------------------------------------------------------------- /src/assets/audio/ui_alert.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/ui_alert.webm -------------------------------------------------------------------------------- /src/assets/audio/ui_chime.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/ui_chime.webm -------------------------------------------------------------------------------- /src/assets/audio/ui_successful_valid.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/ui_successful_valid.webm -------------------------------------------------------------------------------- /src/assets/audio/ui_unsuccessful_invalid.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/audio/ui_unsuccessful_invalid.webm -------------------------------------------------------------------------------- /src/assets/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | /* purgecss start ignore */ 4 | 5 | body { 6 | @apply bg-black; 7 | @apply text-white; 8 | @apply leading-normal; 9 | @apply h-screen; 10 | @apply accent-red; 11 | } 12 | 13 | .btn { 14 | @apply font-normal; 15 | @apply border; 16 | @apply border-gray; 17 | @apply rounded; 18 | @apply px-2; 19 | @apply py-1; 20 | } 21 | .btn:disabled, 22 | .btn.disabled { 23 | @apply opacity-50; 24 | @apply cursor-not-allowed; 25 | } 26 | .btn:not(:disabled, .disabled):hover { 27 | @apply border-white; 28 | } 29 | 30 | .highlight { 31 | @apply ring-4; 32 | @apply ring-lightBlue; 33 | @apply ring-opacity-90; 34 | @apply animate-ping; 35 | animation: ring-pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite; 36 | } 37 | 38 | @keyframes ring-pulse { 39 | 0%, 40 | 100% { 41 | box-shadow: var(--tw-ring-inset) 0 0 0 42 | calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 43 | } 44 | 50% { 45 | box-shadow: var(--tw-ring-inset) 0 0 0 46 | calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); 47 | } 48 | } 49 | 50 | .max-h-modal { 51 | max-height: calc(100vh - 4rem); 52 | } 53 | 54 | .tippy-box { 55 | @apply bg-darkGray; 56 | @apply border; 57 | @apply border-black; 58 | @apply leading-snug; 59 | max-width: 15rem !important; 60 | } 61 | 62 | .tippy-arrow { 63 | @apply text-darkGray; 64 | bottom: -1px; 65 | } 66 | 67 | .notyf__toast { 68 | @apply text-sm; 69 | max-width: 220px; 70 | } 71 | 72 | .notyf__icon { 73 | display: none; 74 | } 75 | 76 | /* purgecss end ignore */ 77 | 78 | @tailwind components; 79 | 80 | @tailwind utilities; 81 | -------------------------------------------------------------------------------- /src/assets/tiles/absorber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/absorber.png -------------------------------------------------------------------------------- /src/assets/tiles/absorber_charge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/absorber_charge.png -------------------------------------------------------------------------------- /src/assets/tiles/basic_projector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/basic_projector.png -------------------------------------------------------------------------------- /src/assets/tiles/battery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/battery.png -------------------------------------------------------------------------------- /src/assets/tiles/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/blank.png -------------------------------------------------------------------------------- /src/assets/tiles/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/border.png -------------------------------------------------------------------------------- /src/assets/tiles/colonists1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/colonists1.png -------------------------------------------------------------------------------- /src/assets/tiles/colonists2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/colonists2.png -------------------------------------------------------------------------------- /src/assets/tiles/colonists3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/colonists3.png -------------------------------------------------------------------------------- /src/assets/tiles/cursor_build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/cursor_build.png -------------------------------------------------------------------------------- /src/assets/tiles/cursor_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/cursor_cancel.png -------------------------------------------------------------------------------- /src/assets/tiles/cursor_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/cursor_move.png -------------------------------------------------------------------------------- /src/assets/tiles/cursor_reflector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/cursor_reflector.png -------------------------------------------------------------------------------- /src/assets/tiles/direction-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/direction-indicator.png -------------------------------------------------------------------------------- /src/assets/tiles/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/disabled.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_armored_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_armored_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_armored_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_armored_2.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_burrowed_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_burrowed_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_burrowed_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_burrowed_2.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_burrower_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_burrower_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_burrower_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_burrower_2.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_drone_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_drone_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_drone_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_drone_2.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_flyer_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_flyer_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_flyer_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_flyer_2.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_volatile_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_volatile_1.png -------------------------------------------------------------------------------- /src/assets/tiles/enemy_volatile_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/enemy_volatile_2.png -------------------------------------------------------------------------------- /src/assets/tiles/exclamation_double.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/exclamation_double.png -------------------------------------------------------------------------------- /src/assets/tiles/exclamation_single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/exclamation_single.png -------------------------------------------------------------------------------- /src/assets/tiles/exclamation_triple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/exclamation_triple.png -------------------------------------------------------------------------------- /src/assets/tiles/factory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/factory.png -------------------------------------------------------------------------------- /src/assets/tiles/farm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/farm.png -------------------------------------------------------------------------------- /src/assets/tiles/farm_growth_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/farm_growth_1.png -------------------------------------------------------------------------------- /src/assets/tiles/farm_growth_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/farm_growth_2.png -------------------------------------------------------------------------------- /src/assets/tiles/farm_growth_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/farm_growth_3.png -------------------------------------------------------------------------------- /src/assets/tiles/fertile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/fertile.png -------------------------------------------------------------------------------- /src/assets/tiles/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/floor.png -------------------------------------------------------------------------------- /src/assets/tiles/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/food.png -------------------------------------------------------------------------------- /src/assets/tiles/laser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_4split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_4split.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_burst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_burst.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_player_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_player_down.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_player_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_player_left.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_player_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_player_right.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_player_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_player_up.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_reflected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_reflected.png -------------------------------------------------------------------------------- /src/assets/tiles/laser_split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/laser_split.png -------------------------------------------------------------------------------- /src/assets/tiles/machinery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/machinery.png -------------------------------------------------------------------------------- /src/assets/tiles/metal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/metal.png -------------------------------------------------------------------------------- /src/assets/tiles/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/mine.png -------------------------------------------------------------------------------- /src/assets/tiles/mining_spot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/mining_spot.png -------------------------------------------------------------------------------- /src/assets/tiles/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/mountain.png -------------------------------------------------------------------------------- /src/assets/tiles/ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/ore.png -------------------------------------------------------------------------------- /src/assets/tiles/outline_dashed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/outline_dashed.png -------------------------------------------------------------------------------- /src/assets/tiles/outline_exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/outline_exclamation.png -------------------------------------------------------------------------------- /src/assets/tiles/outline_solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/outline_solid.png -------------------------------------------------------------------------------- /src/assets/tiles/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/player.png -------------------------------------------------------------------------------- /src/assets/tiles/power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/power.png -------------------------------------------------------------------------------- /src/assets/tiles/powerplant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/powerplant.png -------------------------------------------------------------------------------- /src/assets/tiles/projector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/projector.png -------------------------------------------------------------------------------- /src/assets/tiles/reflector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/reflector.png -------------------------------------------------------------------------------- /src/assets/tiles/residence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/residence.png -------------------------------------------------------------------------------- /src/assets/tiles/road_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_0.png -------------------------------------------------------------------------------- /src/assets/tiles/road_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_1.png -------------------------------------------------------------------------------- /src/assets/tiles/road_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_10.png -------------------------------------------------------------------------------- /src/assets/tiles/road_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_11.png -------------------------------------------------------------------------------- /src/assets/tiles/road_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_12.png -------------------------------------------------------------------------------- /src/assets/tiles/road_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_13.png -------------------------------------------------------------------------------- /src/assets/tiles/road_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_14.png -------------------------------------------------------------------------------- /src/assets/tiles/road_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_15.png -------------------------------------------------------------------------------- /src/assets/tiles/road_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_2.png -------------------------------------------------------------------------------- /src/assets/tiles/road_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_3.png -------------------------------------------------------------------------------- /src/assets/tiles/road_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_4.png -------------------------------------------------------------------------------- /src/assets/tiles/road_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_5.png -------------------------------------------------------------------------------- /src/assets/tiles/road_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_6.png -------------------------------------------------------------------------------- /src/assets/tiles/road_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_7.png -------------------------------------------------------------------------------- /src/assets/tiles/road_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_8.png -------------------------------------------------------------------------------- /src/assets/tiles/road_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/road_9.png -------------------------------------------------------------------------------- /src/assets/tiles/rubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/rubble.png -------------------------------------------------------------------------------- /src/assets/tiles/shield.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/shield.xcf -------------------------------------------------------------------------------- /src/assets/tiles/shield_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/shield_1.png -------------------------------------------------------------------------------- /src/assets/tiles/shield_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/shield_2.png -------------------------------------------------------------------------------- /src/assets/tiles/shield_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/shield_3.png -------------------------------------------------------------------------------- /src/assets/tiles/shield_generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/shield_generator.png -------------------------------------------------------------------------------- /src/assets/tiles/skull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/skull.png -------------------------------------------------------------------------------- /src/assets/tiles/solarpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/solarpanel.png -------------------------------------------------------------------------------- /src/assets/tiles/splitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/splitter.png -------------------------------------------------------------------------------- /src/assets/tiles/splitter_advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/splitter_advanced.png -------------------------------------------------------------------------------- /src/assets/tiles/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/target.png -------------------------------------------------------------------------------- /src/assets/tiles/tent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/tent.png -------------------------------------------------------------------------------- /src/assets/tiles/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/wall.png -------------------------------------------------------------------------------- /src/assets/tiles/wall_crumbling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/wall_crumbling.png -------------------------------------------------------------------------------- /src/assets/tiles/wall_damaged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/wall_damaged.png -------------------------------------------------------------------------------- /src/assets/tiles/warehouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/warehouse.png -------------------------------------------------------------------------------- /src/assets/tiles/water_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_0.png -------------------------------------------------------------------------------- /src/assets/tiles/water_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_15.png -------------------------------------------------------------------------------- /src/assets/tiles/water_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_4.png -------------------------------------------------------------------------------- /src/assets/tiles/water_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_5.png -------------------------------------------------------------------------------- /src/assets/tiles/water_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_6.png -------------------------------------------------------------------------------- /src/assets/tiles/water_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_7.png -------------------------------------------------------------------------------- /src/assets/tiles/water_corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/water_corner.png -------------------------------------------------------------------------------- /src/assets/tiles/windmill-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windmill-1.png -------------------------------------------------------------------------------- /src/assets/tiles/windmill-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windmill-2.png -------------------------------------------------------------------------------- /src/assets/tiles/windmill-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windmill-3.png -------------------------------------------------------------------------------- /src/assets/tiles/windmill-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windmill-4.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_factory_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_factory_1.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_factory_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_factory_2.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_mine_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_mine_1.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_mine_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_mine_2.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_residence_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_residence_1.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_residence_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_residence_2.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_residence_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_residence_3.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_tent_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_tent_1.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_warehouse_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_warehouse_1.png -------------------------------------------------------------------------------- /src/assets/tiles/windows_warehouse_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oatmealproblem/reflector/df8884f9eab9054a77ae2bc783b9b4a8c58b2095/src/assets/tiles/windows_warehouse_2.png -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | import palette from "./data/colors.json"; 2 | 3 | const colors = { 4 | background: palette.black, 5 | backgroundDay: palette.darkerBrown, 6 | backgroundNight: palette.darkerBlue, 7 | text: palette.white, 8 | primary: palette.red, 9 | secondary: palette.lightBlue, 10 | invalid: palette.red, 11 | warning: palette.yellow, 12 | blueprint: palette.lightBlue, 13 | player: palette.white, 14 | inactiveBuilding: palette.gray, 15 | activeBuilding: palette.lessLightGray, 16 | mineral: palette.brown, 17 | mountain: palette.darkBrown, 18 | enemyUnit: palette.purple, 19 | enemyBuilding: palette.darkPurple, 20 | water: palette.blue, 21 | laser: palette.red, 22 | power: palette.yellow, 23 | ground: palette.darkGray, 24 | food: palette.green, 25 | }; 26 | 27 | export default colors; 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { Direction, Pos } from "./types"; 2 | 3 | export const VERSION = "alpha-3.1.0-unstable"; 4 | export const PLAYER_ID = "PLAYER"; 5 | export const CURSOR_ID = "UI_CURSOR"; 6 | 7 | export const BUILDING_RANGE = 2; 8 | export const PROJECTOR_RANGE = 2; 9 | export const STARTING_MORALE = 10; 10 | export const VICTORY_POPULATION = 30; 11 | export const NEW_COLONISTS_PER_DAY = 3; 12 | export const BASE_LASER_STRENGTH = 100; 13 | 14 | export const WAVE_SIZE_CONSTANT = 5; 15 | export const WAVE_SIZE_POPULATION_MULTIPLIER = 2 / 3; 16 | export const WAVE_SIZE_DAY_MULTIPLIER = 2; 17 | 18 | export const MAP_HEIGHT = 48; 19 | export const MAP_WIDTH = MAP_HEIGHT; 20 | export const TILE_SIZE = 24; 21 | export const SIDE_BAR_CSS_WIDTH = "256px"; 22 | export const HEADER_CSS_HEIGHT = "33px"; 23 | export const BUILD_MENU_CSS_HEIGHT = "34px"; 24 | export const MAP_CSS_WIDTH = `min(calc(100vw - ${SIDE_BAR_CSS_WIDTH} - ${SIDE_BAR_CSS_WIDTH}), calc(100vh - ${BUILD_MENU_CSS_HEIGHT} - ${HEADER_CSS_HEIGHT}), ${ 25 | TILE_SIZE * MAP_WIDTH 26 | }px)`; 27 | export const HEADER_CSS_WIDTH = `calc(${MAP_CSS_WIDTH} + ${SIDE_BAR_CSS_WIDTH} + ${SIDE_BAR_CSS_WIDTH})`; 28 | 29 | export const TURNS_PER_DAY = 96; 30 | export const TURNS_PER_NIGHT = TURNS_PER_DAY / 2; 31 | export const END_OF_NIGHT_ENEMY_SPAWNING_BUFFER = 32 | Math.max(MAP_HEIGHT, MAP_WIDTH) / 2; 33 | export const BASE_IMMIGRATION_RATE = TURNS_PER_DAY; 34 | export const VICTORY_ON_TURN = TURNS_PER_DAY * 10; 35 | 36 | export const FOOD_PER_COLONIST = 1; 37 | export const FARM_PRODUCTION = 5; 38 | export const FARM_WORK = TURNS_PER_DAY / 2; 39 | export const MINE_WORK = 4; 40 | export const FACTORY_WORK = 8; 41 | export const REACTOR_PRODUCTION = 10; 42 | 43 | export const PRIORITY_MARKER = 35; 44 | export const PRIORITY_LASER = 30; 45 | export const PRIORITY_BUILDING_HIGH_DETAIL = 25; 46 | export const PRIORITY_BUILDING_HIGH = 20; 47 | export const PRIORITY_PLAYER = 16; 48 | export const PRIORITY_UNIT = 15; 49 | export const PRIORITY_BUILDING_LOW_DETAIL = 10; 50 | export const PRIORITY_BUILDING_LOW = 5; 51 | export const PRIORITY_TERRAIN = 0; 52 | 53 | export const FONT_FAMILY = "Nova Mono"; 54 | 55 | export const TRANSPARENT = "transparent"; 56 | 57 | export const RIGHT = { dx: 1, dy: 0 }; 58 | export const DOWN = { dx: 0, dy: 1 }; 59 | export const LEFT = { dx: -1, dy: 0 }; 60 | export const UP = { dx: 0, dy: -1 }; 61 | 62 | export const DEMO_PAUSE_MICRO = 10; 63 | export const DEMO_PAUSE_SHORT = 500; 64 | export const DEMO_PAUSE_LONG = 1000; 65 | 66 | export const EMPTY_POS: Pos = { x: 0, y: 0 }; 67 | export const EMPTY_DIR: Direction = { dx: 0, dy: 0 }; 68 | 69 | export const MAX_TURNS_TO_SAVE = 20; 70 | -------------------------------------------------------------------------------- /src/data/buildingCategories.ts: -------------------------------------------------------------------------------- 1 | import { TemplateName } from "../types/TemplateName"; 2 | 3 | export enum BuildingCategoryCode { 4 | Work = "WORK", 5 | Power = "POWER", 6 | Defense = "DEFENSE", 7 | Housing = "HOUSING", 8 | Infrastructure = "Infrastructure", 9 | } 10 | 11 | export interface BuildingCategory { 12 | code: BuildingCategoryCode; 13 | label: string; 14 | description: string; 15 | blueprints: TemplateName[]; 16 | } 17 | 18 | const buildingCategories: BuildingCategory[] = [ 19 | { 20 | code: BuildingCategoryCode.Work, 21 | label: "Work", 22 | description: "Buildings that provide jobs for your colonists.", 23 | blueprints: [ 24 | "BLUEPRINT_FARM", 25 | "BLUEPRINT_MINING_SPOT", 26 | "BLUEPRINT_MINE", 27 | "BLUEPRINT_FACTORY", 28 | ], 29 | }, 30 | { 31 | code: BuildingCategoryCode.Power, 32 | label: "Power", 33 | description: "Buildings that produce power.", 34 | blueprints: [ 35 | "BLUEPRINT_WINDMILL", 36 | "BLUEPRINT_SOLAR_PANEL", 37 | "BLUEPRINT_REACTOR", 38 | ], 39 | }, 40 | { 41 | code: BuildingCategoryCode.Defense, 42 | label: "Defense", 43 | description: "Defense and laser-manipulating buildings.", 44 | blueprints: [ 45 | "BLUEPRINT_WALL", 46 | "BLUEPRINT_PROJECTOR_BASIC", 47 | "BLUEPRINT_PROJECTOR_ADVANCED", 48 | "BLUEPRINT_SPLITTER_HORIZONTAL", 49 | "BLUEPRINT_SPLITTER_ADVANCED", 50 | "BLUEPRINT_ABSORBER", 51 | "BLUEPRINT_SHIELD_GENERATOR", 52 | ], 53 | }, 54 | { 55 | code: BuildingCategoryCode.Housing, 56 | label: "Housing", 57 | description: "Places for your colonists to sleep.", 58 | blueprints: ["BLUEPRINT_TENT", "BLUEPRINT_RESIDENCE"], 59 | }, 60 | { 61 | code: BuildingCategoryCode.Infrastructure, 62 | label: "Infrastructure", 63 | description: "Roads and storage.", 64 | blueprints: ["BLUEPRINT_ROAD", "BLUEPRINT_BATTERY", "BLUEPRINT_WAREHOUSE"], 65 | }, 66 | ]; 67 | 68 | export default buildingCategories; 69 | -------------------------------------------------------------------------------- /src/data/colonistStatuses.ts: -------------------------------------------------------------------------------- 1 | export enum ColonistStatusCode { 2 | Sleeping = "SLEEPING", 3 | Wandering = "WANDERING", 4 | GoingToWork = "GOING_TO_WORK", 5 | CannotFindPathToWork = "CANNOT_FIND_PATH_TO_WORK", 6 | Working = "WORKING", 7 | MissingResources = "MISSING_RESOURCES", 8 | GoingHome = "GOING_HOME", 9 | CannotFindPathHome = "CANNOT_FIND_PATH_HOME", 10 | } 11 | 12 | export interface ColonistStatus { 13 | code: ColonistStatusCode; 14 | label: string; 15 | } 16 | 17 | const colonistStatuses: Record = { 18 | [ColonistStatusCode.Sleeping]: { 19 | code: ColonistStatusCode.Sleeping, 20 | label: "Sleeping", 21 | }, 22 | [ColonistStatusCode.Wandering]: { 23 | code: ColonistStatusCode.Wandering, 24 | label: "Wandering", 25 | }, 26 | [ColonistStatusCode.GoingToWork]: { 27 | code: ColonistStatusCode.GoingToWork, 28 | label: "Going to work", 29 | }, 30 | [ColonistStatusCode.CannotFindPathToWork]: { 31 | code: ColonistStatusCode.CannotFindPathToWork, 32 | label: "Cannot find path to work", 33 | }, 34 | [ColonistStatusCode.Working]: { 35 | code: ColonistStatusCode.Working, 36 | label: "Working", 37 | }, 38 | [ColonistStatusCode.MissingResources]: { 39 | code: ColonistStatusCode.MissingResources, 40 | label: "Missing resources", 41 | }, 42 | [ColonistStatusCode.GoingHome]: { 43 | code: ColonistStatusCode.GoingHome, 44 | label: "Going home", 45 | }, 46 | [ColonistStatusCode.CannotFindPathHome]: { 47 | code: ColonistStatusCode.CannotFindPathHome, 48 | label: "Cannot find path home", 49 | }, 50 | }; 51 | 52 | export default colonistStatuses; 53 | -------------------------------------------------------------------------------- /src/data/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "black": "#111111", 3 | "darkGray": "#333333", 4 | "gray": "#555555", 5 | "lessLightGray": "#888888", 6 | "lightGray": "#AAAAAA", 7 | "white": "#FFFFFF", 8 | "opaqueWhite": "#DDDDDD80", 9 | "red": "#f44336", 10 | "lightBlue": "#5C6BC0", 11 | "lighterBlue": "#99a8ff", 12 | "blue": "#3949ab", 13 | "darkerBlue": "#16161d", 14 | "brown": "#bf772f", 15 | "darkBrown": "#6d4c41", 16 | "darkerBrown": "#1c1817", 17 | "purple": "#cb44fc", 18 | "darkPurple": "#512DA8", 19 | "green": "#336635", 20 | "yellow": "#FFD54F" 21 | } 22 | -------------------------------------------------------------------------------- /src/data/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import Settings from "../types/Settings"; 2 | import { ControlCode } from "../types/ControlCode"; 3 | 4 | const defaultSettings: Settings = { 5 | cursorModifierKey: "shift", 6 | fireKeyActivatesAiming: true, 7 | aimInSameDirectionToFire: false, 8 | unmodifiedAiming: true, 9 | unmodifiedBuilding: false, 10 | musicVolume: 50, 11 | sfxVolume: 50, 12 | clickToMove: "ADJACENT", 13 | keybindings: { 14 | [ControlCode.Fire]: ["f"], 15 | 16 | [ControlCode.Up]: ["w", "up"], 17 | [ControlCode.Down]: ["s", "down"], 18 | [ControlCode.Left]: ["a", "left"], 19 | [ControlCode.Right]: ["d", "right"], 20 | 21 | [ControlCode.Menu]: ["F10"], 22 | [ControlCode.Menu1]: ["1"], 23 | [ControlCode.Menu2]: ["2"], 24 | [ControlCode.Menu3]: ["3"], 25 | [ControlCode.Menu4]: ["4"], 26 | [ControlCode.Menu5]: ["5"], 27 | [ControlCode.Menu6]: ["6"], 28 | [ControlCode.Menu7]: ["7"], 29 | [ControlCode.Menu8]: ["8"], 30 | [ControlCode.Menu9]: ["9"], 31 | [ControlCode.Menu0]: ["0"], 32 | [ControlCode.RotateBuilding]: ["r"], 33 | 34 | [ControlCode.QuickAction]: ["space"], 35 | [ControlCode.PlaceReflectorA]: ["/"], 36 | [ControlCode.PlaceReflectorB]: ["\\"], 37 | [ControlCode.RemoveReflector]: ["x"], 38 | [ControlCode.ClearAllReflectors]: ["e"], 39 | [ControlCode.RemoveBuilding]: ["backspace"], 40 | [ControlCode.Rebuild]: ["r"], 41 | [ControlCode.ToggleJobs]: ["j"], 42 | 43 | [ControlCode.Wait]: ["z"], 44 | [ControlCode.Back]: ["q", "esc"], 45 | [ControlCode.Undo]: ["ctrl+z"], 46 | [ControlCode.Help]: ["?"], 47 | [ControlCode.ZoomIn]: ["+", "="], 48 | [ControlCode.ZoomOut]: ["-", "_"], 49 | [ControlCode.Center]: ["c"], 50 | [ControlCode.Recenter]: ["h"], 51 | [ControlCode.FocusJobPriorities]: ["p"], 52 | [ControlCode.DismissNotifications]: ["`"], 53 | 54 | [ControlCode.FocusTutorials]: ["t"], 55 | [ControlCode.ToggleTutorials]: ["shift+t"], 56 | [ControlCode.DismissTutorial]: ["backspace"], 57 | }, 58 | }; 59 | 60 | export default defaultSettings; 61 | -------------------------------------------------------------------------------- /src/data/jobTypes.ts: -------------------------------------------------------------------------------- 1 | export enum JobTypeCode { 2 | Mines = "MINES", 3 | MiningSpots = "MINING_SPOTS", 4 | Farms = "FARMS", 5 | Factories = "FACTORIES", 6 | } 7 | 8 | export interface JobType { 9 | code: JobTypeCode; 10 | label: string; 11 | description: string; 12 | } 13 | 14 | const jobTypes: Record = { 15 | [JobTypeCode.MiningSpots]: { 16 | code: JobTypeCode.MiningSpots, 17 | label: "Mining Spots", 18 | description: "Jobs at mining spots to produce metal.", 19 | }, 20 | [JobTypeCode.Mines]: { 21 | code: JobTypeCode.Mines, 22 | label: "Mines", 23 | description: "Jobs at mines to produce metal while consuming power.", 24 | }, 25 | [JobTypeCode.Farms]: { 26 | code: JobTypeCode.Farms, 27 | label: "Farms", 28 | description: "Jobs at farms to produce food.", 29 | }, 30 | [JobTypeCode.Factories]: { 31 | code: JobTypeCode.Factories, 32 | label: "Factories", 33 | description: "Jobs at factories to produce machinery from metal and power.", 34 | }, 35 | }; 36 | 37 | export default jobTypes; 38 | -------------------------------------------------------------------------------- /src/data/mapTypes.ts: -------------------------------------------------------------------------------- 1 | import { TemplateName } from "../types/TemplateName"; 2 | 3 | interface MapType { 4 | terrainWeights: { 5 | water: number; 6 | fertile: number; 7 | ground: number; 8 | ore: number; 9 | mountain: number; 10 | }; 11 | smoothness: number; 12 | enemyWeightMultipliers: Partial>; 13 | } 14 | 15 | const mapTypes: Record = { 16 | standard: { 17 | terrainWeights: { 18 | water: 15, 19 | fertile: 10, 20 | ground: 60, 21 | ore: 1.5, 22 | mountain: 13.5, 23 | }, 24 | smoothness: 12, 25 | enemyWeightMultipliers: { ENEMY_ARMORED: 1.5 }, 26 | }, 27 | marsh: { 28 | terrainWeights: { 29 | water: 30, 30 | fertile: 33, 31 | ground: 35, 32 | ore: 1, 33 | mountain: 1, 34 | }, 35 | smoothness: 6, 36 | enemyWeightMultipliers: { ENEMY_FLYER: 2 }, 37 | }, 38 | badlands: { 39 | terrainWeights: { 40 | water: 1, 41 | fertile: 1.5, 42 | ground: 65.5, 43 | ore: 5, 44 | mountain: 27, 45 | }, 46 | smoothness: 6, 47 | enemyWeightMultipliers: { ENEMY_BURROWER: 2 }, 48 | }, 49 | plains: { 50 | terrainWeights: { 51 | water: 5, 52 | fertile: 10, 53 | ground: 80, 54 | ore: 1.5, 55 | mountain: 3.5, 56 | }, 57 | smoothness: 15, 58 | enemyWeightMultipliers: { ENEMY_ARMORED: 1.5 }, 59 | }, 60 | mesa: { 61 | terrainWeights: { 62 | water: 1, 63 | fertile: 1.5, 64 | ground: 65.5, 65 | ore: 5, 66 | mountain: 27, 67 | }, 68 | smoothness: 15, 69 | enemyWeightMultipliers: { ENEMY_BURROWER: 2 }, 70 | }, 71 | lakes: { 72 | terrainWeights: { 73 | water: 30, 74 | fertile: 33, 75 | ground: 35, 76 | ore: 1, 77 | mountain: 1, 78 | }, 79 | smoothness: 15, 80 | enemyWeightMultipliers: { ENEMY_FLYER: 2 }, 81 | }, 82 | }; 83 | 84 | export default mapTypes; 85 | -------------------------------------------------------------------------------- /src/data/resources.ts: -------------------------------------------------------------------------------- 1 | import colors from "../colors"; 2 | import { FOOD_PER_COLONIST } from "../constants"; 3 | 4 | export enum ResourceCode { 5 | Food = "FOOD", 6 | Power = "POWER", 7 | Metal = "METAL", 8 | Machinery = "MACHINERY", 9 | } 10 | 11 | export interface Resource { 12 | code: ResourceCode; 13 | label: string; 14 | icon: string; 15 | color: string; 16 | description: string; 17 | } 18 | 19 | const resources: Record = { 20 | [ResourceCode.Power]: { 21 | code: ResourceCode.Power, 22 | label: "Power", 23 | icon: "power", 24 | color: colors.power, 25 | description: 26 | "Power is used by many jobs and buildings. It can be produced automatically by some buildings, or by colonists working at a reactor.", 27 | }, 28 | [ResourceCode.Food]: { 29 | code: ResourceCode.Food, 30 | label: "Food", 31 | icon: "food", 32 | color: colors.food, 33 | description: `Food is grown on farms. Every night, each colonist needs to eat ${FOOD_PER_COLONIST} food, otherwise you lose morale.`, 34 | }, 35 | [ResourceCode.Metal]: { 36 | code: ResourceCode.Metal, 37 | label: "Metal", 38 | icon: "metal", 39 | color: colors.mineral, 40 | description: 41 | "Metal is used to build basic buildings, and can be turned into machinery at a factory.", 42 | }, 43 | [ResourceCode.Machinery]: { 44 | code: ResourceCode.Machinery, 45 | label: "Machinery", 46 | icon: "machinery", 47 | color: colors.activeBuilding, 48 | description: 49 | "Machinery is used to build advanced buildings. It is produced from metal by colonists working at factories.", 50 | }, 51 | }; 52 | 53 | export default resources; 54 | -------------------------------------------------------------------------------- /src/data/templates/index.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../../types"; 2 | import { TemplateName } from "../../types/TemplateName"; 3 | import blueprints from "./blueprints"; 4 | import buildings from "./buildings"; 5 | import enemies from "./enemies"; 6 | import lasers from "./lasers"; 7 | import misc from "./misc"; 8 | import terrain from "./terrain"; 9 | 10 | const templates = { 11 | ...enemies, 12 | ...misc, 13 | ...blueprints, 14 | ...buildings, 15 | ...terrain, 16 | ...lasers, 17 | } as Record>; 18 | 19 | export default templates; 20 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export function useBoolean( 4 | initialValue: boolean 5 | ): [boolean, () => void, () => void, () => void] { 6 | const [value, setValue] = useState(initialValue); 7 | const setTrue = useCallback(() => setValue(true), [setValue]); 8 | const setFalse = useCallback(() => setValue(false), [setValue]); 9 | const toggle = useCallback(() => setValue(!value), [setValue, value]); 10 | return [value, setTrue, setFalse, toggle]; 11 | } 12 | 13 | export function useInterval(callback: () => void, ms: number) { 14 | useEffect(() => { 15 | const handle = setInterval(callback, ms); 16 | return () => clearInterval(handle); 17 | }, [callback, ms]); 18 | } 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reflector: Laser Defense 5 | 6 | 7 | 8 |
9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | import "notyf/notyf.min.css"; 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import { IntlProvider } from "react-intl"; 6 | import Modal from "react-modal"; 7 | import { Provider } from "react-redux"; 8 | import "./assets/style.css"; 9 | import messages from "./messages"; 10 | import store from "./state/store"; 11 | import GameProvider from "./ui/GameProvider"; 12 | import HotkeysProvider from "./ui/HotkeysProvider"; 13 | import pages from "./ui/pages"; 14 | import Router from "./ui/Router"; 15 | import SettingsProvider from "./ui/SettingsProvider"; 16 | 17 | const app = ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | const target = document.getElementById("root"); 32 | 33 | if (target) { 34 | Modal.setAppElement(target); 35 | ReactDOM.render(app, target); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/audio/DummyAudio.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { Pos } from "../../types"; 3 | import { DEFAULT_OPTIONS, SoundOptions } from "./Audio"; 4 | 5 | export default class DummyAudio { 6 | public currentMusicName = ""; 7 | 8 | load() {} 9 | 10 | play(soundName: string, options: SoundOptions = DEFAULT_OPTIONS) {} 11 | 12 | playAtPos( 13 | soundName: string, 14 | pos: Pos, 15 | options: SoundOptions = DEFAULT_OPTIONS 16 | ) {} 17 | 18 | setListenerPos(pos: Pos) {} 19 | 20 | loop(soundName: string, options: SoundOptions = DEFAULT_OPTIONS) {} 21 | 22 | static makePositionalLoopKey(soundName: string, pos: Pos) {} 23 | 24 | loopAtPos( 25 | soundName: string, 26 | pos: Pos, 27 | options: SoundOptions = DEFAULT_OPTIONS 28 | ) {} 29 | 30 | stopAtPos(soundName: string, pos: Pos) {} 31 | 32 | stop(sound: string) {} 33 | 34 | stopAll({ stopMusic = false }) {} 35 | 36 | playMusic(song: "night" | "day") {} 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/audio/index.ts: -------------------------------------------------------------------------------- 1 | import Audio from "./Audio"; 2 | 3 | const audio = new Audio(); 4 | audio.load(); 5 | export default audio; 6 | -------------------------------------------------------------------------------- /src/lib/building.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { Pos, RawState } from "../types"; 3 | import WrappedState from "../types/WrappedState"; 4 | import { areConditionsMet } from "./conditions"; 5 | import { getDistance } from "./geometry"; 6 | import { rangeFromTo } from "./math"; 7 | 8 | export function findValidPositions( 9 | state: WrappedState, 10 | buildFroms: { 11 | pos: Pos; 12 | range: number; 13 | }[], 14 | canPlace: (state: RawState, pos: Pos) => boolean 15 | ): Pos[] { 16 | const results: Pos[] = []; 17 | for (const buildFrom of buildFroms) { 18 | for (const dx of rangeFromTo(-buildFrom.range, buildFrom.range + 1)) { 19 | for (const dy of rangeFromTo(-buildFrom.range, buildFrom.range + 1)) { 20 | const pos = { x: buildFrom.pos.x + dx, y: buildFrom.pos.y + dy }; 21 | if (canPlace(state.raw, pos)) { 22 | results.push(pos); 23 | } 24 | } 25 | } 26 | } 27 | return results; 28 | } 29 | 30 | export function canPlaceReflector(state: WrappedState, pos: Pos) { 31 | const isPositionBlocked = state.select 32 | .entitiesAtPosition(pos) 33 | .some((e) => e.blocking && e.blocking.moving); 34 | const activeProjectors = state.select 35 | .entitiesWithComps("pos", "projector") 36 | .filter((e) => areConditionsMet(state, e, e.projector.condition)); 37 | return ( 38 | !isPositionBlocked && 39 | activeProjectors.some( 40 | (projector) => 41 | getDistance(pos, projector.pos) <= projector.projector.range 42 | ) 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/entities.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | import templates from "../data/templates"; 3 | import { Entity } from "../types/Entity"; 4 | import { TemplateName } from "../types/TemplateName"; 5 | import WrappedState from "../types/WrappedState"; 6 | import { getPosKey } from "./geometry"; 7 | 8 | export function createEntityFromTemplate( 9 | templateId: TemplateName, 10 | additionalComps?: Partial 11 | ) { 12 | const template = templates[templateId]; 13 | const parent: Entity = template.parentTemplate 14 | ? createEntityFromTemplate(template.parentTemplate) 15 | : { id: "tempId", template: "NONE" }; 16 | 17 | return { 18 | ...parent, 19 | ...templates[templateId], 20 | ...additionalComps, 21 | id: `${templateId}_${nanoid()}`, 22 | template: templateId, 23 | }; 24 | } 25 | 26 | export function resetEntitiesByCompAndPos(state: WrappedState): void { 27 | const entitiesByComp: Record> = {}; 28 | const entitiesByPosition: Record> = {}; 29 | 30 | for (const entity of state.select.entityList()) { 31 | if (entity.pos) { 32 | const key = getPosKey(entity.pos); 33 | entitiesByPosition[key] = entitiesByPosition[key] || new Set(); 34 | entitiesByPosition[key].add(entity.id); 35 | } 36 | 37 | for (const key in entity) { 38 | if ( 39 | entity[key as keyof Entity] && 40 | key !== "id" && 41 | key !== "template" && 42 | key !== "parentTemplate" 43 | ) { 44 | entitiesByComp[key] = entitiesByComp[key] || new Set(); 45 | entitiesByComp[key].add(entity.id); 46 | } 47 | } 48 | } 49 | 50 | state.setRaw({ 51 | ...state.raw, 52 | entitiesByComp, 53 | entitiesByPosition, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/gameSave.ts: -------------------------------------------------------------------------------- 1 | import * as idb from "idb-keyval"; 2 | import { MAX_TURNS_TO_SAVE } from "../constants"; 3 | import { RawState } from "../types"; 4 | 5 | export function save(state: RawState): void { 6 | const stateToSave: RawState = { 7 | ...state, 8 | isAutoMoving: false, 9 | entitiesByComp: {}, 10 | entitiesByPosition: {}, 11 | }; 12 | idb 13 | .setMany([ 14 | [`save-${state.time.turn}`, stateToSave], 15 | ["save-latest", stateToSave], 16 | ]) 17 | .catch((e) => { 18 | console.error("save failed", e); 19 | }) 20 | .then(() => idb.del(`save-${state.time.turn - MAX_TURNS_TO_SAVE}`)) 21 | .catch((e) => { 22 | console.error("save deletion failed", e); 23 | }); 24 | } 25 | 26 | export function load( 27 | saveName: string = "save-latest" 28 | ): Promise { 29 | return idb.get(saveName).catch((e) => { 30 | // eslint-disable-next-line no-alert 31 | alert( 32 | "Failed to load the game. This may be because you are in private browsing or incognito mode. You may still play, but your progress will not be saved, and the undo feature will be unavailable." 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/makeLevel.ts: -------------------------------------------------------------------------------- 1 | import { PLAYER_ID } from "../constants"; 2 | import WrappedState from "../types/WrappedState"; 3 | import generateMap from "./generateMap"; 4 | 5 | export default function makeLevel(state: WrappedState): WrappedState { 6 | state.act.removeEntities( 7 | state.select 8 | .entityList() 9 | .filter((e) => e.pos && e.id !== PLAYER_ID) 10 | .map((e) => e.id) 11 | ); 12 | for (const entity of generateMap(state.raw.mapType)) { 13 | state.act.addEntity(entity); 14 | } 15 | return state; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/math.ts: -------------------------------------------------------------------------------- 1 | export function rangeFromTo(from: number, to: number): number[] { 2 | const results: number[] = []; 3 | for (let i = from; i < to; i++) { 4 | results.push(i); 5 | } 6 | return results; 7 | } 8 | 9 | export function rangeTo(to: number): number[] { 10 | return rangeFromTo(0, to); 11 | } 12 | 13 | export function calcPercentile( 14 | sortedArray: number[], 15 | percentile: number 16 | ): number { 17 | const index = Math.min( 18 | sortedArray.length, 19 | Math.max(0, Math.round((sortedArray.length * percentile) / 100)) 20 | ); 21 | return sortedArray[index]; 22 | } 23 | 24 | export function sum(...numbers: number[]) { 25 | return numbers.reduce((acc, cur) => acc + cur, 0); 26 | } 27 | 28 | export function round(value: number, precision: number = 0) { 29 | return Math.round(value * 10 ** precision) / 10 ** precision; 30 | } 31 | 32 | export function distribute(value: number, buckets: number) { 33 | return rangeTo(buckets).map((i) => 34 | i < value % buckets 35 | ? Math.ceil(value / buckets) 36 | : Math.floor(value / buckets) 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/notifications.ts: -------------------------------------------------------------------------------- 1 | import { Notyf } from "notyf"; 2 | import colors from "../colors"; 3 | 4 | const notifications = new Notyf({ 5 | position: { x: "right", y: "bottom" }, 6 | types: [ 7 | { type: "error", background: colors.invalid }, 8 | { type: "success", background: colors.secondary }, 9 | { type: "info", background: colors.inactiveBuilding }, 10 | ], 11 | duration: 10000, 12 | dismissible: true, 13 | }); 14 | 15 | export default notifications; 16 | -------------------------------------------------------------------------------- /src/lib/rng.ts: -------------------------------------------------------------------------------- 1 | import { sum } from "./math"; 2 | 3 | export function choose(array: T[]): T { 4 | return array[Math.floor(Math.random() * array.length)]; 5 | } 6 | 7 | export function randomInt(exclusiveMaximum: number) { 8 | return Math.floor(Math.random() * exclusiveMaximum); 9 | } 10 | 11 | export function pickWeighted(options: [T, number][]): T { 12 | const total = sum(...options.map((optionAndWeight) => optionAndWeight[1])); 13 | const pick = randomInt(total); 14 | let currentVal = 0; 15 | for (const [option, weight] of options) { 16 | currentVal += weight; 17 | if (pick < currentVal) return option; 18 | } 19 | return options[options.length - 1][0]; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/tutorials.ts: -------------------------------------------------------------------------------- 1 | import tutorials from "../data/tutorials"; 2 | import { Action } from "../types"; 3 | import WrappedState from "../types/WrappedState"; 4 | 5 | export function processTutorials( 6 | prevState: WrappedState, 7 | nextState: WrappedState, 8 | action: Action 9 | ): void { 10 | let isDirty = false; 11 | 12 | for (const activeTutorial of nextState.select.activeTutorials()) { 13 | const tutorial = tutorials[activeTutorial.id]; 14 | const step = tutorial.steps[activeTutorial.step]; 15 | if (!step) { 16 | nextState.act.completeTutorial(tutorial.id); 17 | isDirty = true; 18 | } else if (step.checkForCompletion(prevState, nextState, action)) { 19 | nextState.act.completeTutorialStep(tutorial.id); 20 | isDirty = true; 21 | } 22 | } 23 | 24 | for (const tutorial of Object.values(tutorials)) { 25 | if ( 26 | !nextState.select.isTutorialCompleted(tutorial.id) && 27 | !nextState.select.isTutorialActive(tutorial.id) && 28 | tutorial.triggerSelector(nextState.raw) 29 | ) { 30 | nextState.act.startTutorial(tutorial.id); 31 | isDirty = true; 32 | } 33 | } 34 | 35 | if (isDirty) { 36 | nextState.save(nextState.raw); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | // @ts-ignore 3 | import tiles from "url:../assets/tiles/*.png"; // eslint-disable-line import/no-unresolved 4 | import colors from "../colors"; 5 | import { MAP_HEIGHT, MAP_WIDTH, TILE_SIZE } from "../constants"; 6 | import Renderer from "./Renderer"; 7 | 8 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 9 | 10 | const renderer = new Renderer({ 11 | gridWidth: MAP_WIDTH, 12 | gridHeight: MAP_HEIGHT, 13 | tileWidth: TILE_SIZE, 14 | tileHeight: TILE_SIZE, 15 | appWidth: MAP_WIDTH * TILE_SIZE, 16 | appHeight: MAP_HEIGHT * TILE_SIZE, 17 | backgroundColor: colors.background, 18 | autoCenterEnabled: true, 19 | }); 20 | 21 | renderer.setAppSize(window.innerWidth, window.innerHeight); 22 | renderer.zoomIn(); 23 | renderer.load(tiles); 24 | renderer.start(); 25 | 26 | export default renderer; 27 | -------------------------------------------------------------------------------- /src/state/actions/addEntity.ts: -------------------------------------------------------------------------------- 1 | import { Required } from "ts-toolbelt/out/Object/Required"; 2 | import { createAction } from "typesafe-actions"; 3 | import { Entity } from "../../types"; 4 | import { getPosKey } from "../../lib/geometry"; 5 | import { registerHandler } from "../handleAction"; 6 | import WrappedState from "../../types/WrappedState"; 7 | import { retargetLaserOnReflectorChange } from "../../lib/lasers"; 8 | 9 | const addEntity = createAction("ADD_ENTITY")(); 10 | export default addEntity; 11 | 12 | function addEntityHandler( 13 | wrappedState: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const entity = action.payload; 17 | 18 | const { entities, entitiesByPosition, entitiesByComp } = wrappedState.raw; 19 | 20 | for (const key in entity) { 21 | if ( 22 | entity[key as keyof Entity] && 23 | key !== "id" && 24 | key !== "template" && 25 | key !== "parentTemplate" 26 | ) { 27 | entitiesByComp[key] = entitiesByComp[key] || new Set(); 28 | entitiesByComp[key].add(entity.id); 29 | } 30 | } 31 | 32 | if (entity.pos) { 33 | const key = getPosKey(entity.pos); 34 | entitiesByPosition[key] = entitiesByPosition[key] || new Set(); 35 | entitiesByPosition[key].add(entity.id); 36 | } 37 | 38 | if (entity.pos && entity.display) { 39 | wrappedState.renderer.addEntity( 40 | entity as Required 41 | ); 42 | } 43 | 44 | entities[entity.id] = entity; 45 | 46 | if (entity.reflector && entity.pos) { 47 | retargetLaserOnReflectorChange(wrappedState, entity.pos); 48 | } 49 | } 50 | 51 | registerHandler(addEntityHandler, addEntity); 52 | -------------------------------------------------------------------------------- /src/state/actions/autoMove.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Direction } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const autoMove = createAction("AUTO_MOVE")(); 7 | export default autoMove; 8 | 9 | function autoMoveHandler(state: WrappedState): void { 10 | const player = state.select.player(); 11 | if (!player) return; 12 | 13 | const path = state.select 14 | .entitiesWithComps("pathPreview", "pos") 15 | .sort((a, b) => a.pathPreview.index - b.pathPreview.index); 16 | 17 | if (!path.length) { 18 | state.act.cancelAutoMove(); 19 | return; 20 | } 21 | 22 | state.setRaw({ 23 | ...state.raw, 24 | isAutoMoving: true, 25 | }); 26 | 27 | const [next, ...rest] = path; 28 | const direction: Direction = { 29 | dx: next.pos.x - player.pos.x, 30 | dy: next.pos.y - player.pos.y, 31 | }; 32 | 33 | if (Math.abs(direction.dx) + Math.abs(direction.dy) !== 1) { 34 | console.error( 35 | `Invalid auto-move from ${player.pos.x},${player.pos.y} to ${next.pos.x},${next.pos.y}` 36 | ); 37 | state.act.cancelAutoMove(); 38 | return; 39 | } 40 | 41 | state.act.move({ entityId: player.id, ...direction }); 42 | state.act.removeEntity(next.id); 43 | if (rest.length === 0) { 44 | state.act.cancelAutoMove(); 45 | } else if (state.select.areEnemiesPresent()) { 46 | state.act.cancelAutoMove(); 47 | state.act.logMessage({ 48 | message: "Enemies present, movement canceled", 49 | type: "info", 50 | }); 51 | } 52 | } 53 | 54 | registerHandler(autoMoveHandler, autoMove); 55 | -------------------------------------------------------------------------------- /src/state/actions/blueprintCancel.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const blueprintCancel = createAction("BLUEPRINT_CANCEL")(); 6 | export default blueprintCancel; 7 | 8 | function blueprintCancelHandler( 9 | state: WrappedState, 10 | action: ReturnType 11 | ): void { 12 | state.act.removeEntities( 13 | state.select 14 | .entityList() 15 | .filter((e) => e.validMarker) 16 | .map((e) => e.id) 17 | ); 18 | const entity = state.select.blueprint(); 19 | if (!entity) return; 20 | state.act.removeEntities([entity.id]); 21 | state.act.bordersUpdate(); 22 | } 23 | 24 | registerHandler(blueprintCancelHandler, blueprintCancel); 25 | -------------------------------------------------------------------------------- /src/state/actions/blueprintMove.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import colors from "../../colors"; 3 | import { registerHandler } from "../handleAction"; 4 | import { Pos } from "../../types"; 5 | import WrappedState from "../../types/WrappedState"; 6 | import { arePositionsEqual } from "../../lib/geometry"; 7 | import { areConditionsMet } from "../../lib/conditions"; 8 | 9 | const blueprintMove = createAction("BLUEPRINT_MOVE")<{ 10 | to: Pos; 11 | }>(); 12 | export default blueprintMove; 13 | 14 | function blueprintMoveHandler( 15 | state: WrappedState, 16 | action: ReturnType 17 | ): void { 18 | const blueprint = state.select.blueprint(); 19 | if (!blueprint || !blueprint.pos) return; 20 | const validPositions = state.select 21 | .entitiesWithComps("validMarker", "pos") 22 | .map((e) => e.pos); 23 | const { to: newPos } = action.payload; 24 | 25 | const isValid = validPositions.some((validPos) => 26 | arePositionsEqual(validPos, newPos) 27 | ); 28 | 29 | const invalidMessage = isValid 30 | ? "" 31 | : blueprint.blueprint.validityConditions.filter( 32 | (vc) => 33 | !areConditionsMet(state, { ...blueprint, pos: newPos }, vc.condition) 34 | )[0]?.invalidMessage ?? ""; 35 | 36 | state.act.updateEntity({ 37 | id: blueprint.id, 38 | pos: newPos, 39 | display: { 40 | ...blueprint.display, 41 | color: isValid ? colors.blueprint : colors.invalid, 42 | }, 43 | warning: invalidMessage ? { text: invalidMessage } : undefined, 44 | }); 45 | 46 | state.act.bordersUpdate(); 47 | } 48 | 49 | registerHandler(blueprintMoveHandler, blueprintMove); 50 | -------------------------------------------------------------------------------- /src/state/actions/bordersDraw.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { DOWN, LEFT, RIGHT, UP } from "../../constants"; 3 | import { areConditionsMet } from "../../lib/conditions"; 4 | import { createEntityFromTemplate } from "../../lib/entities"; 5 | import { 6 | getPositionsWithinRange, 7 | getPositionToDirection, 8 | getPosKey, 9 | } from "../../lib/geometry"; 10 | import { Pos } from "../../types"; 11 | import WrappedState from "../../types/WrappedState"; 12 | import { registerHandler } from "../handleAction"; 13 | 14 | const bordersDraw = createAction("bordersDraw")(); 15 | export default bordersDraw; 16 | 17 | function bordersDrawHandler( 18 | state: WrappedState, 19 | action: ReturnType 20 | ): void { 21 | const positionsInRange: Record = {}; 22 | const projectors = state.select 23 | .entitiesWithComps("pos", "projector") 24 | .filter((e) => areConditionsMet(state, e, e.projector.condition)); 25 | for (const entity of projectors) { 26 | positionsInRange[getPosKey(entity.pos)] = entity.pos; 27 | for (const pos of getPositionsWithinRange( 28 | entity.pos, 29 | entity.projector.range 30 | )) { 31 | positionsInRange[getPosKey(pos)] = pos; 32 | } 33 | } 34 | 35 | const posKeys = new Set(Object.keys(positionsInRange)); 36 | for (const pos of Object.values(positionsInRange)) { 37 | const posToNorth = getPositionToDirection(pos, UP); 38 | const posToSouth = getPositionToDirection(pos, DOWN); 39 | const posToEast = getPositionToDirection(pos, RIGHT); 40 | const posToWest = getPositionToDirection(pos, LEFT); 41 | 42 | const posToNorthIsInRange = posKeys.has(getPosKey(posToNorth)); 43 | const posToSouthIsInRange = posKeys.has(getPosKey(posToSouth)); 44 | const posToEastIsInRange = posKeys.has(getPosKey(posToEast)); 45 | const posToWestIsInRange = posKeys.has(getPosKey(posToWest)); 46 | 47 | if (!posToNorthIsInRange) { 48 | state.act.addEntity(createEntityFromTemplate("UI_BORDER_NORTH", { pos })); 49 | } 50 | if (!posToSouthIsInRange) { 51 | state.act.addEntity(createEntityFromTemplate("UI_BORDER_SOUTH", { pos })); 52 | } 53 | if (!posToEastIsInRange) { 54 | state.act.addEntity(createEntityFromTemplate("UI_BORDER_EAST", { pos })); 55 | } 56 | if (!posToWestIsInRange) { 57 | state.act.addEntity(createEntityFromTemplate("UI_BORDER_WEST", { pos })); 58 | } 59 | } 60 | } 61 | 62 | registerHandler(bordersDrawHandler, bordersDraw); 63 | -------------------------------------------------------------------------------- /src/state/actions/bordersRemove.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const bordersRemove = createAction("bordersRemove")(); 6 | export default bordersRemove; 7 | 8 | function bordersRemoveHandler( 9 | state: WrappedState, 10 | action: ReturnType 11 | ): void { 12 | state.act.removeEntities( 13 | state.select.entitiesWithComps("border").map((e) => e.id) 14 | ); 15 | } 16 | 17 | registerHandler(bordersRemoveHandler, bordersRemove); 18 | -------------------------------------------------------------------------------- /src/state/actions/bordersUpdate.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { registerHandler } from "../handleAction"; 5 | 6 | const bordersUpdate = createAction("bordersUpdate")(); 7 | export default bordersUpdate; 8 | 9 | function bordersUpdateHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const hasBorders = state.select.entitiesWithComps("border").length > 0; 14 | const shouldDrawBorders = Boolean( 15 | state.select.blueprint()?.blueprint?.showBorders || 16 | state.select.entitiesWithComps("laser").length 17 | ); 18 | if (shouldDrawBorders) { 19 | const bordersKey = state.select 20 | .entitiesWithComps("projector", "pos") 21 | .filter((e) => areConditionsMet(state, e, e.projector.condition)) 22 | .map((e) => `${e.pos.x},${e.pos.y},${e.projector.range}`) 23 | .sort() 24 | .join("-"); 25 | if (bordersKey !== state.raw.bordersKey) { 26 | state.setRaw({ ...state.raw, bordersKey }); 27 | state.act.bordersRemove(); 28 | state.act.bordersDraw(); 29 | } 30 | } else if (hasBorders) { 31 | state.setRaw({ ...state.raw, bordersKey: null }); 32 | state.act.bordersRemove(); 33 | } 34 | } 35 | 36 | registerHandler(bordersUpdateHandler, bordersUpdate); 37 | -------------------------------------------------------------------------------- /src/state/actions/cancelAutoMove.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const cancelAutoMove = createAction("CANCEL_AUTO_MOVE")(); 6 | export default cancelAutoMove; 7 | 8 | function cancelAutoMoveHandler(state: WrappedState): void { 9 | state.setRaw({ 10 | ...state.raw, 11 | isAutoMoving: false, 12 | }); 13 | } 14 | 15 | registerHandler(cancelAutoMoveHandler, cancelAutoMove); 16 | -------------------------------------------------------------------------------- /src/state/actions/clearReflectors.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const clearReflectors = createAction("CLEAR_REFLECTORS")(); 6 | export default clearReflectors; 7 | 8 | function clearReflectorsHandler( 9 | state: WrappedState, 10 | action: ReturnType 11 | ): void { 12 | const reflectors = state.select.entitiesWithComps("reflector", "pos"); 13 | reflectors.forEach((reflector) => { 14 | state.act.removeReflector(reflector.pos); 15 | }); 16 | } 17 | 18 | registerHandler(clearReflectorsHandler, clearReflectors); 19 | -------------------------------------------------------------------------------- /src/state/actions/completeTutorial.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { TutorialId } from "../../types/TutorialId"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const completeTutorial = createAction("COMPLETE_TUTORIAL")(); 7 | export default completeTutorial; 8 | 9 | function completeTutorialHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const tutorialId = action.payload; 14 | const tutorialsState = state.raw.tutorials; 15 | state.setRaw({ 16 | ...state.raw, 17 | tutorials: { 18 | completed: [...tutorialsState.completed, tutorialId], 19 | active: tutorialsState.active.filter((t) => t.id !== tutorialId), 20 | }, 21 | }); 22 | } 23 | 24 | registerHandler(completeTutorialHandler, completeTutorial); 25 | -------------------------------------------------------------------------------- /src/state/actions/completeTutorialStep.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import tutorials from "../../data/tutorials"; 3 | import { registerHandler } from "../handleAction"; 4 | import { TutorialId } from "../../types/TutorialId"; 5 | import WrappedState from "../../types/WrappedState"; 6 | 7 | const completeTutorialStep = createAction( 8 | "COMPLETE_TUTORIAL_STEP" 9 | )(); 10 | export default completeTutorialStep; 11 | 12 | function completeTutorialStepHandler( 13 | state: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const tutorialId = action.payload; 17 | const tutorialsState = state.raw.tutorials; 18 | const activeTutorial = tutorialsState.active.find((t) => t.id === tutorialId); 19 | if (!activeTutorial) { 20 | console.error( 21 | `Tried to complete a step in an inactive tutorial: ${tutorialId}` 22 | ); 23 | return; 24 | } 25 | const isLastStep = 26 | activeTutorial.step >= tutorials[tutorialId].steps.length - 1; 27 | if (isLastStep) { 28 | state.act.completeTutorial(tutorialId); 29 | } else { 30 | state.setRaw({ 31 | ...state.raw, 32 | tutorials: { 33 | ...tutorialsState, 34 | active: tutorialsState.active.map((t) => 35 | t.id === tutorialId ? { ...t, step: activeTutorial.step + 1 } : t 36 | ), 37 | }, 38 | }); 39 | } 40 | } 41 | 42 | registerHandler(completeTutorialStepHandler, completeTutorialStep); 43 | -------------------------------------------------------------------------------- /src/state/actions/continueVictory.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const continueVictory = createAction("CONTINUE_VICTORY")(); 6 | export default continueVictory; 7 | 8 | function continueVictoryHandler(state: WrappedState): void { 9 | if (state.raw.victory) { 10 | state.setRaw({ 11 | ...state.raw, 12 | gameOver: false, 13 | }); 14 | } 15 | } 16 | 17 | registerHandler(continueVictoryHandler, continueVictory); 18 | -------------------------------------------------------------------------------- /src/state/actions/cycleReflector.ts: -------------------------------------------------------------------------------- 1 | import { Required } from "ts-toolbelt/out/Object/Required"; 2 | import { createAction } from "typesafe-actions"; 3 | import { registerHandler } from "../handleAction"; 4 | import { Entity, Pos } from "../../types"; 5 | import WrappedState from "../../types/WrappedState"; 6 | import { canPlaceReflector } from "../../lib/building"; 7 | import { createEntityFromTemplate } from "../../lib/entities"; 8 | 9 | const cycleReflector = createAction("CYCLE_REFLECTOR")(); 10 | export default cycleReflector; 11 | 12 | function cycleReflectorHandler( 13 | state: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const pos = action.payload; 17 | const entitiesAtPos = state.select.entitiesAtPosition(pos); 18 | const reflectorAtPos = entitiesAtPos.find((e) => 19 | Boolean(e.reflector) 20 | ) as Required; 21 | if (reflectorAtPos) { 22 | if (reflectorAtPos.reflector.type === "/") { 23 | state.act.rotateEntity(reflectorAtPos); 24 | } else { 25 | state.act.removeEntity(reflectorAtPos.id); 26 | } 27 | } else { 28 | const player = state.select.player(); 29 | if (!player) return; 30 | if (canPlaceReflector(state, pos)) { 31 | state.act.addEntity( 32 | createEntityFromTemplate("REFLECTOR_UP_RIGHT", { pos }) 33 | ); 34 | } 35 | } 36 | } 37 | 38 | registerHandler(cycleReflectorHandler, cycleReflector); 39 | -------------------------------------------------------------------------------- /src/state/actions/deactivateWeapon.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { PLAYER_ID } from "../../constants"; 5 | 6 | const deactivateWeapon = createAction("DEACTIVATE_WEAPON")(); 7 | export default deactivateWeapon; 8 | 9 | function deactivateWeaponHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const laserState = state.select.laserState(); 14 | if (["ACTIVE", "FIRING"].includes(laserState)) { 15 | state.act.removeEntities( 16 | state.select 17 | .entitiesWithComps("laser") 18 | .filter((e) => e.laser.source === PLAYER_ID) 19 | .map((e) => e.id) 20 | ); 21 | if (state.select.entitiesWithComps("laser").length === 0) { 22 | state.audio.stop("laser_active"); 23 | } 24 | if (laserState === "ACTIVE") { 25 | state.setRaw({ 26 | ...state.raw, 27 | laserState: "READY", 28 | }); 29 | state.audio.play("laser_cancel"); 30 | } 31 | } 32 | state.act.bordersUpdate(); 33 | } 34 | 35 | registerHandler(deactivateWeaponHandler, deactivateWeapon); 36 | -------------------------------------------------------------------------------- /src/state/actions/decreaseJobPriority.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { JobTypeCode } from "../../data/jobTypes"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const decreaseJobPriority = createAction( 7 | "DECREASE_JOB_PRIORITY" 8 | )(); 9 | export default decreaseJobPriority; 10 | 11 | function decreaseJobPriorityHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ) { 15 | const previousPriority = state.select.jobPriority(action.payload); 16 | const newPriority = previousPriority + 1; 17 | const jobPriorities: Record = { 18 | ...state.raw.jobPriorities, 19 | }; 20 | (Object.keys(jobPriorities) as JobTypeCode[]).forEach((jobType) => { 21 | if (jobPriorities[jobType] === newPriority) { 22 | jobPriorities[jobType] = previousPriority; 23 | jobPriorities[action.payload] = newPriority; 24 | } 25 | }); 26 | state.setRaw({ 27 | ...state.raw, 28 | jobPriorities, 29 | }); 30 | } 31 | 32 | registerHandler(decreaseJobPriorityHandler, decreaseJobPriority); 33 | -------------------------------------------------------------------------------- /src/state/actions/destroyPos.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import colors from "../../colors"; 3 | import { Pos } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { registerHandler } from "../handleAction"; 6 | 7 | const destroyPos = createAction("destroyPos")<{ target: Pos; from: Pos }>(); 8 | export default destroyPos; 9 | 10 | function destroyPosHandler( 11 | state: WrappedState, 12 | action: ReturnType 13 | ): void { 14 | const entitiesAtTarget = state.select.entitiesAtPosition( 15 | action.payload.target 16 | ); 17 | const entitiesAtFrom = state.select.entitiesAtPosition(action.payload.from); 18 | 19 | const shieldEntity = entitiesAtTarget.find((e) => e.shield); 20 | const shield = shieldEntity && shieldEntity.shield; 21 | if ( 22 | shield && 23 | !entitiesAtFrom.some( 24 | (e) => e.shield && e.shield.generator === shield.generator 25 | ) 26 | ) { 27 | state.act.shieldDischarge(shield.generator); 28 | state.renderer.flash(action.payload.target, colors.secondary); 29 | state.audio.playAtPos("power_off", action.payload.target, { volume: 2 }); 30 | } else { 31 | entitiesAtTarget 32 | .filter((e) => e.destructible) 33 | .forEach(({ id }) => state.act.destroy(id)); 34 | } 35 | } 36 | 37 | registerHandler(destroyPosHandler, destroyPos); 38 | -------------------------------------------------------------------------------- /src/state/actions/executeRemoveBuilding.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Pos } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { arePositionsEqual } from "../../lib/geometry"; 6 | import { executeEffect } from "../../data/effects"; 7 | 8 | const executeRemoveBuilding = createAction("EXECUTE_REMOVE_BUILDING")(); 9 | export default executeRemoveBuilding; 10 | 11 | function executeRemoveBuildingHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const removingTarget = state.select 16 | .entitiesWithComps("pos", "building") 17 | .find((entity) => arePositionsEqual(entity.pos, action.payload)); 18 | if (!removingTarget) return; 19 | if (removingTarget.housing && removingTarget.pos) { 20 | if ( 21 | state.select 22 | .colonists() 23 | .some((colonist) => arePositionsEqual(colonist.pos, removingTarget.pos)) 24 | ) { 25 | state.act.logMessage({ 26 | message: "You cannot remove houses with colonists inside", 27 | type: "error", 28 | }); 29 | return; 30 | } 31 | } 32 | if ( 33 | removingTarget.temperature && 34 | removingTarget.temperature.status !== "normal" 35 | ) { 36 | state.act.logMessage({ 37 | message: "You cannot remove overheating buildings", 38 | type: "error", 39 | }); 40 | return; 41 | } 42 | state.act.removeEntity(removingTarget.id); 43 | 44 | // remove job disabler 45 | state.select 46 | .entitiesAtPosition(action.payload) 47 | .filter((e) => e.jobDisabler) 48 | .forEach((e) => state.act.removeEntity(e.id)); 49 | 50 | // remove absorber charge 51 | state.select 52 | .entitiesAtPosition(action.payload) 53 | .filter((e) => e.template === "UI_ABSORBER_CHARGE") 54 | .forEach((e) => state.act.removeEntity(e.id)); 55 | 56 | // remove farm growth 57 | executeEffect("CLEAR_BUILDING_FARM_GROWTH", state, undefined, removingTarget); 58 | 59 | // remove window 60 | executeEffect("CLEAR_UI_WINDOW", state, undefined, removingTarget); 61 | 62 | // discharge shield 63 | if (removingTarget.shieldGenerator) { 64 | state.act.shieldDischarge(removingTarget.id); 65 | } 66 | } 67 | 68 | registerHandler(executeRemoveBuildingHandler, executeRemoveBuilding); 69 | -------------------------------------------------------------------------------- /src/state/actions/farmGrowthUpdateTile.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { Entity, Pos } from "../../types"; 5 | import { createEntityFromTemplate } from "../../lib/entities"; 6 | 7 | const farmGrowthUpdateTile = createAction("farmGrowthUpdateTile")(); 8 | export default farmGrowthUpdateTile; 9 | 10 | function farmGrowthUpdateTileHandler( 11 | state: WrappedState, 12 | action: ReturnType 13 | ): void { 14 | const entitiesAtPosition = state.select.entitiesAtPosition(action.payload); 15 | const farm = entitiesAtPosition.find((e) => e.template === "BUILDING_FARM"); 16 | if (!farm) return; 17 | let growth: Entity | undefined = entitiesAtPosition.find( 18 | (e) => e.template === "BUILDING_FARM_GROWTH" 19 | ); 20 | if (!growth) { 21 | growth = createEntityFromTemplate("BUILDING_FARM_GROWTH", { 22 | pos: action.payload, 23 | }); 24 | state.act.addEntity(growth); 25 | } 26 | if (!growth || !growth.display) return; 27 | const newTile = getGrowthTile(farm); 28 | if (newTile !== growth.display.tile) { 29 | state.act.updateEntity({ 30 | id: growth.id, 31 | display: { 32 | ...growth.display, 33 | tile: newTile, 34 | }, 35 | }); 36 | } 37 | } 38 | 39 | function getGrowthTile(farm: Entity) { 40 | const progress = 41 | (farm.jobProvider?.workContributed ?? 0) / 42 | (farm.jobProvider?.workRequired ?? 1); 43 | if (progress === 0) { 44 | return "blank"; 45 | } else if (progress < 0.25) { 46 | return "farm_growth_1"; 47 | } else if (progress < 0.5) { 48 | return "farm_growth_2"; 49 | } else if (progress < 0.75) { 50 | return "farm_growth_3"; 51 | } else { 52 | return "farm"; 53 | } 54 | } 55 | 56 | registerHandler(farmGrowthUpdateTileHandler, farmGrowthUpdateTile); 57 | -------------------------------------------------------------------------------- /src/state/actions/fireWeapon.ts: -------------------------------------------------------------------------------- 1 | import { RNG } from "rot-js"; 2 | import { createAction } from "typesafe-actions"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { PLAYER_ID } from "../../constants"; 6 | import { fromPosKey, getPosKey } from "../../lib/geometry"; 7 | 8 | const fireWeapon = createAction("FIRE_WEAPON")<{ source: string }>(); 9 | export default fireWeapon; 10 | 11 | function fireWeaponHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const isPlayer = action.payload.source === PLAYER_ID; 16 | if (isPlayer) { 17 | if (!state.select.isWeaponActive()) return; 18 | if (!state.select.player()) return; 19 | } 20 | 21 | const lasers = state.select 22 | .entitiesWithComps("laser", "pos") 23 | .filter((e) => e.laser.source === action.payload.source); 24 | 25 | state.renderer.flashGlowAndRemoveGroup(lasers[0].display?.group?.id || ""); 26 | 27 | const positionsToDestroy: string[] = []; 28 | for (const laser of lasers.filter((entity) => !entity.laser.cosmetic)) { 29 | const { pos } = laser; 30 | const entitiesAtPos = state.select.entitiesAtPosition(pos); 31 | for (const entity of entitiesAtPos) { 32 | if (entity.absorber) { 33 | state.act.updateEntity({ 34 | id: entity.id, 35 | absorber: { 36 | ...entity.absorber, 37 | charged: true, 38 | }, 39 | }); 40 | } else if ( 41 | entity.destructible && 42 | entitiesAtPos.some((e) => e.blocking && e.blocking.lasers) 43 | ) { 44 | positionsToDestroy.push(getPosKey(entity.pos)); 45 | } 46 | } 47 | } 48 | 49 | for (const posKey of new Set(positionsToDestroy)) { 50 | state.act.destroyPos({ 51 | target: fromPosKey(posKey), 52 | from: fromPosKey(posKey), 53 | }); 54 | } 55 | 56 | if (isPlayer) { 57 | state.setRaw({ 58 | ...state.raw, 59 | laserState: "FIRING", 60 | }); 61 | state.act.deactivateWeapon(); 62 | } else { 63 | state.act.removeEntities(lasers.map((e) => e.id)); 64 | } 65 | 66 | if (state.select.entitiesWithComps("laser").length === 0) { 67 | state.audio.stop("laser_active"); 68 | } 69 | state.audio.play( 70 | RNG.getItem([ 71 | "laser_shot_1", 72 | "laser_shot_2", 73 | "laser_shot_3", 74 | // "laser_shot_4", 75 | // "laser_shot_5", 76 | // "laser_shot_6", 77 | ]) || "" 78 | ); 79 | 80 | if (isPlayer) { 81 | state.act.playerTookTurn(); 82 | } 83 | } 84 | 85 | registerHandler(fireWeaponHandler, fireWeapon); 86 | -------------------------------------------------------------------------------- /src/state/actions/increaseJobPriority.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { JobTypeCode } from "../../data/jobTypes"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const increaseJobPriority = createAction( 7 | "INCREASE_JOB_PRIORITY" 8 | )(); 9 | export default increaseJobPriority; 10 | 11 | function increaseJobPriorityHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ) { 15 | const previousPriority = state.select.jobPriority(action.payload); 16 | const newPriority = previousPriority - 1; 17 | const jobPriorities: Record = { 18 | ...state.raw.jobPriorities, 19 | }; 20 | (Object.keys(jobPriorities) as JobTypeCode[]).forEach((jobType) => { 21 | if (jobPriorities[jobType] === newPriority) { 22 | jobPriorities[jobType] = previousPriority; 23 | jobPriorities[action.payload] = newPriority; 24 | } 25 | }); 26 | state.setRaw({ 27 | ...state.raw, 28 | jobPriorities, 29 | }); 30 | } 31 | 32 | registerHandler(increaseJobPriorityHandler, increaseJobPriority); 33 | -------------------------------------------------------------------------------- /src/state/actions/loadGame.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import colors from "../../colors"; 3 | import { VERSION } from "../../constants"; 4 | import { resetEntitiesByCompAndPos } from "../../lib/entities"; 5 | import { RawState } from "../../types"; 6 | import WrappedState from "../../types/WrappedState"; 7 | import { registerHandler } from "../handleAction"; 8 | import { cosmeticSystems } from "../systems"; 9 | 10 | const loadGame = createAction("LOAD_GAME")<{ 11 | state: RawState; 12 | }>(); 13 | export default loadGame; 14 | 15 | function loadGameHandler( 16 | state: WrappedState, 17 | action: ReturnType 18 | ): void { 19 | const { state: loadedState } = action.payload; 20 | state.setRaw({ 21 | ...loadedState, 22 | entities: { 23 | ...loadedState.entities, 24 | }, 25 | version: VERSION, 26 | }); 27 | resetEntitiesByCompAndPos(state); 28 | state.renderer.clear(); 29 | state.select 30 | .entitiesWithComps("pos", "display") 31 | .forEach((entity) => state.renderer.addEntity(entity)); 32 | 33 | state.audio.stopAll({ stopMusic: false }); 34 | const playerPos = state.select.playerPos(); 35 | if (playerPos) { 36 | state.audio.setListenerPos(playerPos); 37 | } 38 | const musicName = state.select.isNight() ? "night" : "day"; 39 | if (state.audio.currentMusicName !== musicName) { 40 | state.audio.playMusic(musicName); 41 | } 42 | if (state.select.entitiesWithComps("laser").length > 0) { 43 | state.audio.loop("laser_active", { volume: 0.5 }); 44 | } 45 | 46 | cosmeticSystems.forEach((system) => system(state)); 47 | if (state.select.isNight()) { 48 | state.renderer.setBackgroundColor(colors.backgroundNight); 49 | } else { 50 | state.renderer.setBackgroundColor(colors.backgroundDay); 51 | } 52 | } 53 | 54 | registerHandler(loadGameHandler, loadGame); 55 | -------------------------------------------------------------------------------- /src/state/actions/logEvent.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const logEvent = createAction("LOG_EVENT")<{ 6 | type: string; 7 | count?: number; 8 | }>(); 9 | export default logEvent; 10 | 11 | function logEventHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const { type } = action.payload; 16 | const count = action.payload.count || 1; 17 | state.setRaw({ 18 | ...state.raw, 19 | events: { 20 | ...state.raw.events, 21 | [type]: (state.raw.events[type] || 0) + count, 22 | }, 23 | }); 24 | } 25 | 26 | registerHandler(logEventHandler, logEvent); 27 | -------------------------------------------------------------------------------- /src/state/actions/logMessage.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import notifications from "../../lib/notifications"; 5 | 6 | const logMessage = createAction("LOG_MESSAGE")<{ 7 | message: string; 8 | type: "error" | "success" | "info"; 9 | }>(); 10 | export default logMessage; 11 | 12 | function logMessageHandler( 13 | state: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const { message, type } = action.payload; 17 | const turn = state.select.turn(); 18 | state.setRaw({ 19 | ...state.raw, 20 | messageLog: { 21 | ...state.raw.messageLog, 22 | [turn]: [ 23 | ...(state.raw.messageLog[turn] || []), 24 | { 25 | message, 26 | type, 27 | }, 28 | ], 29 | }, 30 | }); 31 | 32 | // should probably find a better solution to this 33 | // perhaps the same subscription system I'm planning for rendering 34 | notifications.open({ 35 | type, 36 | message, 37 | }); 38 | if (type === "success") { 39 | state.audio.play("ui_chime"); 40 | } else if (type === "error") { 41 | state.audio.play("ui_alert"); 42 | } 43 | } 44 | 45 | registerHandler(logMessageHandler, logMessage); 46 | -------------------------------------------------------------------------------- /src/state/actions/makeMeRich.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { ResourceCode } from "../../data/resources"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const makeMeRich = createAction("MAKE_ME_RICH")(); 7 | export default makeMeRich; 8 | 9 | function makeMeRichHandler(state: WrappedState) { 10 | for (const resource of Object.keys(state.select.resources())) { 11 | state.act.modifyResource({ 12 | resource: resource as ResourceCode, 13 | amount: 1000, 14 | reason: "I'm rich!", 15 | }); 16 | } 17 | } 18 | 19 | registerHandler(makeMeRichHandler, makeMeRich); 20 | -------------------------------------------------------------------------------- /src/state/actions/modifyResource.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { ResourceCode } from "../../data/resources"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { round } from "../../lib/math"; 6 | 7 | const modifyResource = createAction("MODIFY_RESOURCE")<{ 8 | resource: ResourceCode; 9 | amount: number; 10 | reason: string; 11 | }>(); 12 | export default modifyResource; 13 | 14 | function modifyResourceHandler( 15 | state: WrappedState, 16 | action: ReturnType 17 | ): void { 18 | const { resource, amount, reason } = action.payload; 19 | state.setRaw({ 20 | ...state.raw, 21 | resources: { 22 | ...state.raw.resources, 23 | [resource]: round((state.raw.resources[resource] || 0) + amount, 1), 24 | }, 25 | resourceChangesThisTurn: { 26 | ...state.raw.resourceChangesThisTurn, 27 | [resource]: state.raw.resourceChangesThisTurn[resource].some( 28 | (change) => change.reason === reason 29 | ) 30 | ? state.raw.resourceChangesThisTurn[resource].map((change) => 31 | change.reason === reason 32 | ? { reason, amount: change.amount + amount } 33 | : change 34 | ) 35 | : [...state.raw.resourceChangesThisTurn[resource], { reason, amount }], 36 | }, 37 | }); 38 | } 39 | 40 | registerHandler(modifyResourceHandler, modifyResource); 41 | -------------------------------------------------------------------------------- /src/state/actions/newGame.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import makeLevel from "../../lib/makeLevel"; 3 | import { registerHandler } from "../handleAction"; 4 | import { createInitialState } from "../initialState"; 5 | import colonistsSystem from "../systems/colonistsSystem"; 6 | import WrappedState from "../../types/WrappedState"; 7 | 8 | const newGame = createAction("NEW_GAME")<{ mapType: string }>(); 9 | export default newGame; 10 | 11 | function newGameHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | state.setRaw( 16 | createInitialState({ 17 | completedTutorials: state.select.completedTutorials(), 18 | mapType: action.payload.mapType, 19 | }) 20 | ); 21 | state.renderer.clear(); 22 | makeLevel(state); 23 | state.act.loadGame({ state: state.raw }); 24 | state.setRaw({ 25 | ...state.raw, 26 | }); 27 | colonistsSystem(state); 28 | } 29 | 30 | registerHandler(newGameHandler, newGame); 31 | -------------------------------------------------------------------------------- /src/state/actions/playerTookTurn.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { ResourceCode } from "../../data/resources"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { registerHandler } from "../handleAction"; 5 | import { cosmeticSystems, turnEndSystems } from "../systems"; 6 | 7 | const playerTookTurn = createAction("PLAYER_TOOK_TURN")(); 8 | export default playerTookTurn; 9 | 10 | function playerTookTurnHandler( 11 | state: WrappedState, 12 | action: ReturnType 13 | ): void { 14 | state.setRaw({ 15 | ...state.raw, 16 | lastMoveWasFast: false, 17 | }); 18 | 19 | turnEndSystems.forEach((system) => system(state)); 20 | cosmeticSystems.forEach((system) => system(state)); 21 | state.setRaw({ 22 | ...state.raw, 23 | resourceChanges: state.raw.resourceChangesThisTurn, 24 | resourceChangesThisTurn: { 25 | [ResourceCode.Food]: [], 26 | [ResourceCode.Power]: [], 27 | [ResourceCode.Metal]: [], 28 | [ResourceCode.Machinery]: [], 29 | }, 30 | }); 31 | 32 | state.save(state.raw); 33 | } 34 | 35 | registerHandler(playerTookTurnHandler, playerTookTurn); 36 | -------------------------------------------------------------------------------- /src/state/actions/rebuild.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { createEntityFromTemplate } from "../../lib/entities"; 5 | import resources from "../../data/resources"; 6 | import { areConditionsMet } from "../../lib/conditions"; 7 | 8 | const rebuild = createAction("rebuild")(); 9 | export default rebuild; 10 | 11 | function rebuildHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const entity = state.select.entityById(action.payload); 16 | if (!entity || !entity.rebuildable || !entity.pos) return; 17 | const blueprint = createEntityFromTemplate(entity.rebuildable.blueprint, { 18 | pos: entity.pos, 19 | }); 20 | if (!blueprint.blueprint) return; 21 | 22 | const failedConditions = blueprint.blueprint.validityConditions.filter( 23 | (validityCondition) => 24 | !areConditionsMet(state, blueprint, validityCondition.condition) 25 | ); 26 | if (failedConditions.length) { 27 | const message = failedConditions[0] 28 | ? failedConditions[0].invalidMessage 29 | : "Invalid position."; 30 | state.act.logMessage({ message, type: "error" }); 31 | return; 32 | } 33 | 34 | const { cost } = blueprint.blueprint; 35 | if (state.select.canAffordToPay(cost.resource, cost.amount)) { 36 | state.act.removeEntity(entity.id); 37 | state.act.addEntity( 38 | createEntityFromTemplate(blueprint.blueprint.builds, { pos: entity.pos }) 39 | ); 40 | state.act.modifyResource({ 41 | resource: cost.resource, 42 | amount: -cost.amount, 43 | reason: "Rebuild", 44 | }); 45 | state.audio.playAtPos("building_built", entity.pos); 46 | state.act.playerTookTurn(); 47 | } else { 48 | state.act.logMessage({ 49 | message: `Cannot afford building (${cost.amount} ${ 50 | resources[cost.resource].label 51 | })`, 52 | type: "error", 53 | }); 54 | } 55 | } 56 | 57 | registerHandler(rebuildHandler, rebuild); 58 | -------------------------------------------------------------------------------- /src/state/actions/reduceMorale.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const reduceMorale = createAction("REDUCE_MORALE")<{ 6 | amount: number; 7 | }>(); 8 | export default reduceMorale; 9 | 10 | function reduceMoraleHandler( 11 | state: WrappedState, 12 | action: ReturnType 13 | ): void { 14 | state.setRaw({ 15 | ...state.raw, 16 | morale: state.raw.morale - action.payload.amount, 17 | }); 18 | } 19 | 20 | registerHandler(reduceMoraleHandler, reduceMorale); 21 | -------------------------------------------------------------------------------- /src/state/actions/removeEntities.ts: -------------------------------------------------------------------------------- 1 | import { Required } from "ts-toolbelt/out/Object/Required"; 2 | import { createAction } from "typesafe-actions"; 3 | import { registerHandler } from "../handleAction"; 4 | import { Entity } from "../../types"; 5 | import WrappedState from "../../types/WrappedState"; 6 | import { getPosKey } from "../../lib/geometry"; 7 | import { retargetLaserOnReflectorChange } from "../../lib/lasers"; 8 | 9 | const removeEntities = createAction("REMOVE_ENTITIES")(); 10 | export default removeEntities; 11 | 12 | function removeEntitiesHandler( 13 | wrappedState: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const { raw: state } = wrappedState; 17 | const entityIds = action.payload.filter((id) => state.entities[id]); 18 | 19 | let isRemovingReflector = false; 20 | const { entitiesByPosition, entitiesByComp, entities } = state; 21 | 22 | for (const id of entityIds) { 23 | const entity = entities[id]; 24 | for (const key in entity) { 25 | if (key !== "id" && key !== "template" && key !== "parentTemplate") { 26 | entitiesByComp[key] = entitiesByComp[key] || new Set(); 27 | entitiesByComp[key].delete(id); 28 | } 29 | } 30 | if (entity.pos) { 31 | entitiesByPosition[getPosKey(entity.pos)].delete(id); 32 | } 33 | if (entity.pos && entity.display) { 34 | wrappedState.renderer.removeEntity(id); 35 | } 36 | if (entity.reflector) { 37 | isRemovingReflector = true; 38 | } 39 | if (entity.pos && entity.smokeEmitter) { 40 | ( 41 | entity as Required 42 | ).smokeEmitter.emitters.forEach((emitter) => 43 | wrappedState.renderer.removeSmoke( 44 | (entity as Required).pos, 45 | emitter.offset 46 | ) 47 | ); 48 | } 49 | if (entity.pos && entity.audioToggle) { 50 | wrappedState.audio.stopAtPos(entity.audioToggle.soundName, entity.pos); 51 | } 52 | 53 | delete entities[id]; 54 | } 55 | 56 | if (isRemovingReflector) { 57 | retargetLaserOnReflectorChange(wrappedState); 58 | } 59 | } 60 | 61 | registerHandler(removeEntitiesHandler, removeEntities); 62 | -------------------------------------------------------------------------------- /src/state/actions/removeEntity.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const removeEntity = createAction("REMOVE_ENTITY")(); 6 | export default removeEntity; 7 | 8 | function removeEntityHandler( 9 | state: WrappedState, 10 | action: ReturnType 11 | ): void { 12 | state.act.removeEntities([action.payload]); 13 | } 14 | 15 | registerHandler(removeEntityHandler, removeEntity); 16 | -------------------------------------------------------------------------------- /src/state/actions/removeReflector.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Pos } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const removeReflector = createAction("REMOVE_REFLECTOR")(); 7 | export default removeReflector; 8 | 9 | function removeReflectorHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const pos = action.payload; 14 | const entitiesAtPosition = state.select.entitiesAtPosition(pos); 15 | const reflector = entitiesAtPosition.find((e) => e.reflector); 16 | 17 | if (reflector) { 18 | state.act.removeEntity(reflector.id); 19 | } 20 | } 21 | 22 | registerHandler(removeReflectorHandler, removeReflector); 23 | -------------------------------------------------------------------------------- /src/state/actions/resetTutorials.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | const resetTutorials = createAction("RESET_TUTORIALS")(); 6 | export default resetTutorials; 7 | 8 | function resetTutorialsHandler(state: WrappedState): void { 9 | state.setRaw({ 10 | ...state.raw, 11 | tutorials: { 12 | active: [], 13 | completed: [], 14 | }, 15 | }); 16 | } 17 | 18 | registerHandler(resetTutorialsHandler, resetTutorials); 19 | -------------------------------------------------------------------------------- /src/state/actions/roadUpdateTile.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { Pos } from "../../types"; 5 | import { getPositionToDirection } from "../../lib/geometry"; 6 | import { UP, RIGHT, DOWN, LEFT } from "../../constants"; 7 | 8 | const roadUpdateTile = createAction("roadUpdateTile")(); 9 | export default roadUpdateTile; 10 | 11 | function roadUpdateTileHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const pos = action.payload; 16 | const road = state.select.entitiesAtPosition(pos).find((e) => e.road); 17 | if (!road || !road.display) return; 18 | const tileNumber = 19 | 1 * Number(state.select.hasRoad(getPositionToDirection(pos, UP))) + 20 | 2 * Number(state.select.hasRoad(getPositionToDirection(pos, RIGHT))) + 21 | 4 * Number(state.select.hasRoad(getPositionToDirection(pos, DOWN))) + 22 | 8 * Number(state.select.hasRoad(getPositionToDirection(pos, LEFT))); 23 | state.act.updateEntity({ 24 | id: road.id, 25 | display: { ...road.display, tile: `road_${tileNumber}` }, 26 | }); 27 | } 28 | 29 | registerHandler(roadUpdateTileHandler, roadUpdateTile); 30 | -------------------------------------------------------------------------------- /src/state/actions/rotateEntity.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Entity } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { createEntityFromTemplate } from "../../lib/entities"; 6 | import { retargetLaserOnReflectorChange } from "../../lib/lasers"; 7 | 8 | const rotateEntity = createAction("ROTATE_ENTITY")(); 9 | export default rotateEntity; 10 | 11 | function rotateEntityHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const entity = action.payload; 16 | if (!entity.rotatable) return; 17 | state.act.removeEntity(entity.id); 18 | const newEntity = { 19 | ...entity, 20 | ...createEntityFromTemplate(entity.rotatable.rotatesTo), 21 | }; 22 | state.act.addEntity(newEntity); 23 | if (entity.reflector && entity.pos) { 24 | retargetLaserOnReflectorChange(state, entity.pos); 25 | } 26 | } 27 | 28 | registerHandler(rotateEntityHandler, rotateEntity); 29 | -------------------------------------------------------------------------------- /src/state/actions/setAutoMovePath.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Pos } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { createEntityFromTemplate } from "../../lib/entities"; 6 | import { TemplateName } from "../../types/TemplateName"; 7 | 8 | const setAutoMovePath = createAction("SET_AUTO_MOVE_PATH")(); 9 | export default setAutoMovePath; 10 | 11 | function setAutoMovePathHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const pathPreviews = state.select.entitiesWithComps("pathPreview", "pos"); 16 | state.act.removeEntities(pathPreviews.map((e) => e.id)); 17 | 18 | action.payload.forEach((pos, index) => { 19 | const template: TemplateName = 20 | index === 0 || !state.select.areEnemiesPresent() 21 | ? "UI_PATH" 22 | : "UI_PATH_DEEMPHASIZED"; 23 | state.act.addEntity( 24 | createEntityFromTemplate(template, { pos, pathPreview: { index } }) 25 | ); 26 | }); 27 | } 28 | 29 | registerHandler(setAutoMovePathHandler, setAutoMovePath); 30 | -------------------------------------------------------------------------------- /src/state/actions/setJobPriority.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import jobTypes, { JobTypeCode } from "../../data/jobTypes"; 3 | import { registerHandler } from "../handleAction"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const setJobPriority = createAction("SET_JOB_PRIORITY")<{ 7 | jobType: JobTypeCode; 8 | priority: number; 9 | }>(); 10 | export default setJobPriority; 11 | 12 | function setJobPriorityHandler( 13 | state: WrappedState, 14 | action: ReturnType 15 | ): void { 16 | const { jobType: code, priority: newPriority } = action.payload; 17 | const oldPriority = state.select.jobPriority(code); 18 | const newPriorities = { 19 | ...state.raw.jobPriorities, 20 | }; 21 | for (const jobType of Object.values(jobTypes)) { 22 | const priority = newPriorities[jobType.code]; 23 | if (jobType.code === code) { 24 | newPriorities[jobType.code] = newPriority; 25 | } else if (priority > oldPriority && priority <= newPriority) { 26 | newPriorities[jobType.code] = priority - 1; 27 | } else if (priority < oldPriority && priority >= newPriority) { 28 | newPriorities[jobType.code] = priority + 1; 29 | } 30 | } 31 | state.setRaw({ 32 | ...state.raw, 33 | jobPriorities: newPriorities, 34 | }); 35 | } 36 | 37 | registerHandler(setJobPriorityHandler, setJobPriority); 38 | -------------------------------------------------------------------------------- /src/state/actions/shieldDischarge.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { createEntityFromTemplate } from "../../lib/entities"; 3 | import { TemplateName } from "../../types/TemplateName"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { registerHandler } from "../handleAction"; 6 | 7 | const shieldDischarge = createAction("shieldDischarge")(); 8 | export default shieldDischarge; 9 | 10 | function shieldDischargeHandler( 11 | state: WrappedState, 12 | action: ReturnType 13 | ): void { 14 | const entity = state.select.entityById(action.payload); 15 | if ( 16 | entity && 17 | entity.shieldGenerator && 18 | entity.powered && 19 | entity.powered.hasPower 20 | ) { 21 | const currentStrength = entity.shieldGenerator.strength; 22 | if (currentStrength > 0) { 23 | const newStrength = currentStrength - 1; 24 | state.act.updateEntity({ 25 | id: entity.id, 26 | shieldGenerator: { 27 | ...entity.shieldGenerator, 28 | strength: newStrength, 29 | }, 30 | }); 31 | if (newStrength === 0) { 32 | state.act.removeEntities( 33 | state.select 34 | .entitiesWithComps("shield") 35 | .filter((e) => e.shield.generator === action.payload) 36 | .map((e) => e.id) 37 | ); 38 | } else { 39 | const displayedShield = state.select 40 | .entitiesWithComps("shield", "display", "pos") 41 | .find((e) => e.shield.generator === action.payload); 42 | if (displayedShield) { 43 | const newDisplay = createEntityFromTemplate( 44 | `UI_SHIELD_${newStrength}` as TemplateName 45 | ).display; 46 | state.act.updateEntity({ 47 | id: displayedShield.id, 48 | display: newDisplay, 49 | }); 50 | } 51 | } 52 | } 53 | } else { 54 | // generator destroyed or removed or unpowered, clean up shields 55 | if (entity && entity.shieldGenerator) { 56 | state.act.updateEntity({ 57 | id: entity.id, 58 | shieldGenerator: { 59 | ...entity.shieldGenerator, 60 | strength: 0, 61 | }, 62 | }); 63 | } 64 | state.act.removeEntities( 65 | state.select 66 | .entitiesWithComps("shield") 67 | .filter((e) => e.shield.generator === action.payload) 68 | .map((e) => e.id) 69 | ); 70 | } 71 | } 72 | 73 | registerHandler(shieldDischargeHandler, shieldDischarge); 74 | -------------------------------------------------------------------------------- /src/state/actions/startTutorial.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { TutorialId } from "../../types/TutorialId"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const startTutorial = createAction("START_TUTORIAL")(); 7 | export default startTutorial; 8 | 9 | function startTutorialHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const tutorialId = action.payload; 14 | const tutorialsState = state.raw.tutorials; 15 | state.setRaw({ 16 | ...state.raw, 17 | tutorials: { 18 | completed: tutorialsState.completed.includes(tutorialId) 19 | ? tutorialsState.completed.filter((id) => id !== tutorialId) 20 | : tutorialsState.completed, 21 | active: [ 22 | ...tutorialsState.active, 23 | { 24 | id: tutorialId, 25 | step: 0, 26 | }, 27 | ], 28 | }, 29 | }); 30 | } 31 | 32 | registerHandler(startTutorialHandler, startTutorial); 33 | -------------------------------------------------------------------------------- /src/state/actions/temperatureDecrease.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { executeEffect } from "../../data/effects"; 5 | 6 | const temperatureDecrease = createAction("temperatureDecrease")(); 7 | export default temperatureDecrease; 8 | 9 | function temperatureDecreaseHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const entity = state.select.entityById(action.payload); 14 | if (!entity || !entity.temperature) return; 15 | const { status } = entity.temperature; 16 | if (status === "hot") { 17 | state.act.updateEntity({ 18 | id: entity.id, 19 | temperature: { ...entity.temperature, status: "normal" }, 20 | }); 21 | executeEffect("CLEAR_UI_OVERHEATING_HOT", state, undefined, entity); 22 | } else if (status === "very hot") { 23 | state.act.updateEntity({ 24 | id: entity.id, 25 | temperature: { ...entity.temperature, status: "hot" }, 26 | }); 27 | executeEffect("CLEAR_UI_OVERHEATING_VERY_HOT", state, undefined, entity); 28 | executeEffect("SPAWN_UI_OVERHEATING_HOT", state, undefined, entity); 29 | } else if (status === "critical") { 30 | state.act.updateEntity({ 31 | id: entity.id, 32 | temperature: { ...entity.temperature, status: "very hot" }, 33 | }); 34 | executeEffect("CLEAR_UI_OVERHEATING_CRITICAL", state, undefined, entity); 35 | executeEffect("SPAWN_UI_OVERHEATING_VERY_HOT", state, undefined, entity); 36 | } 37 | } 38 | 39 | registerHandler(temperatureDecreaseHandler, temperatureDecrease); 40 | -------------------------------------------------------------------------------- /src/state/actions/temperatureIncrease.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { executeEffect } from "../../data/effects"; 3 | import WrappedState from "../../types/WrappedState"; 4 | import { registerHandler } from "../handleAction"; 5 | 6 | const temperatureIncrease = createAction("temperatureIncrease")(); 7 | export default temperatureIncrease; 8 | 9 | function temperatureIncreaseHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ): void { 13 | const entity = state.select.entityById(action.payload); 14 | if (!entity || !entity.temperature) return; 15 | const { status } = entity.temperature; 16 | if (status === "normal") { 17 | state.act.updateEntity({ 18 | id: entity.id, 19 | temperature: { ...entity.temperature, status: "hot" }, 20 | }); 21 | executeEffect("SPAWN_UI_OVERHEATING_HOT", state, undefined, entity); 22 | } else if (status === "hot") { 23 | state.act.updateEntity({ 24 | id: entity.id, 25 | temperature: { ...entity.temperature, status: "very hot" }, 26 | }); 27 | executeEffect("SPAWN_UI_OVERHEATING_VERY_HOT", state, undefined, entity); 28 | executeEffect("CLEAR_UI_OVERHEATING_HOT", state, undefined, entity); 29 | } else if (status === "very hot") { 30 | state.act.updateEntity({ 31 | id: entity.id, 32 | temperature: { ...entity.temperature, status: "critical" }, 33 | }); 34 | executeEffect("SPAWN_UI_OVERHEATING_CRITICAL", state, undefined, entity); 35 | executeEffect("CLEAR_UI_OVERHEATING_VERY_HOT", state, undefined, entity); 36 | } else if (status === "critical") { 37 | executeEffect(entity.temperature.onOverheat, state, undefined, entity); 38 | } 39 | } 40 | 41 | registerHandler(temperatureIncreaseHandler, temperatureIncrease); 42 | -------------------------------------------------------------------------------- /src/state/actions/toggleDisabled.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { Pos } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | import { createEntityFromTemplate } from "../../lib/entities"; 6 | import { arePositionsEqual } from "../../lib/geometry"; 7 | 8 | const toggleDisabled = createAction("TOGGLE_DISABLED")(); 9 | export default toggleDisabled; 10 | 11 | function toggleDisabledHandler( 12 | state: WrappedState, 13 | action: ReturnType 14 | ): void { 15 | const pos = action.payload; 16 | const disablers = state.select.jobDisablers(); 17 | const disablerAtPos = disablers.find((e) => arePositionsEqual(e.pos, pos)); 18 | if (disablerAtPos) { 19 | state.act.removeEntity(disablerAtPos.id); 20 | } else if (state.select.entitiesAtPosition(pos).some((e) => e.jobProvider)) { 21 | state.act.addEntity(createEntityFromTemplate("UI_JOB_DISABLER", { pos })); 22 | } 23 | } 24 | 25 | registerHandler(toggleDisabledHandler, toggleDisabled); 26 | -------------------------------------------------------------------------------- /src/state/actions/undoTurn.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import { registerHandler } from "../handleAction"; 3 | import { RawState } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | const undoTurn = createAction("UNDO_TURN")(); 7 | export default undoTurn; 8 | 9 | function undoTurnHandler( 10 | state: WrappedState, 11 | action: ReturnType 12 | ) { 13 | if (action.payload) { 14 | state.act.loadGame({ 15 | state: action.payload, 16 | }); 17 | state.act.logMessage({ 18 | message: "Reset to start of last turn.", 19 | type: "success", 20 | }); 21 | } else { 22 | state.act.logMessage({ 23 | message: "Cannot undo any further.", 24 | type: "error", 25 | }); 26 | } 27 | } 28 | 29 | registerHandler(undoTurnHandler, undoTurn); 30 | -------------------------------------------------------------------------------- /src/state/actions/updateEntity.ts: -------------------------------------------------------------------------------- 1 | import has from "has"; 2 | import { Required } from "ts-toolbelt/out/Object/Required"; 3 | import { Object } from "ts-toolbelt"; 4 | import { createAction } from "typesafe-actions"; 5 | import selectors from "../selectors"; 6 | import { Entity } from "../../types"; 7 | import { getPosKey } from "../../lib/geometry"; 8 | import { registerHandler } from "../handleAction"; 9 | import WrappedState from "../../types/WrappedState"; 10 | 11 | const updateEntity = 12 | createAction("UPDATE_ENTITY"), "id">>(); 13 | export default updateEntity; 14 | 15 | function updateEntityHandler( 16 | wrappedState: WrappedState, 17 | action: ReturnType 18 | ): void { 19 | const { raw: state } = wrappedState; 20 | const partial = action.payload; 21 | const prev = selectors.entityById(state, partial.id); 22 | if (!prev) { 23 | console.error("Tried to update nonexistant entity", partial); 24 | return; 25 | } 26 | const entity = { ...prev, ...partial }; 27 | const { entitiesByPosition, entitiesByComp, entities } = state; 28 | 29 | for (const key in partial) { 30 | if (key !== "id" && key !== "template" && key !== "parentTemplate") { 31 | entitiesByComp[key] = entitiesByComp[key] || new Set(); 32 | if (partial[key as keyof Entity]) { 33 | entitiesByComp[key].add(partial.id); 34 | } else { 35 | entitiesByComp[key].delete(partial.id); 36 | } 37 | } 38 | } 39 | 40 | if (has(partial, "pos")) { 41 | if (prev && prev.pos) { 42 | const key = getPosKey(prev.pos); 43 | entitiesByPosition[key].delete(prev.id); 44 | } 45 | if (entity.pos) { 46 | const key = getPosKey(entity.pos); 47 | entitiesByPosition[key] = entitiesByPosition[key] || new Set(); 48 | entitiesByPosition[key].add(entity.id); 49 | } 50 | } 51 | 52 | if (has(partial, "pos") || has(partial, "display")) { 53 | if (entity.pos && entity.display) { 54 | if (!prev.pos || !prev.display) { 55 | wrappedState.renderer.addEntity( 56 | entity as Required 57 | ); 58 | } else { 59 | wrappedState.renderer.updateEntity( 60 | entity as Required 61 | ); 62 | } 63 | } else { 64 | wrappedState.renderer.removeEntity(entity.id); 65 | } 66 | } 67 | 68 | entities[entity.id] = entity; 69 | } 70 | 71 | registerHandler(updateEntityHandler, updateEntity); 72 | -------------------------------------------------------------------------------- /src/state/handleAction.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator, getType } from "typesafe-actions"; 2 | import { Action } from "../types"; 3 | import WrappedState from "../types/WrappedState"; 4 | 5 | type ActionHandler = (state: WrappedState, action: any) => void; 6 | 7 | const handlers: { 8 | [type: string]: ActionHandler; 9 | } = {}; 10 | 11 | export function registerHandler( 12 | handler: ActionHandler, 13 | actionCreator: ActionCreator 14 | ) { 15 | handlers[getType(actionCreator)] = handler; 16 | } 17 | 18 | export default function handleAction( 19 | state: WrappedState, 20 | action: Action 21 | ): WrappedState { 22 | const handler = handlers[action.type]; 23 | if (handler) { 24 | handler(state, action); 25 | } 26 | return state; 27 | } 28 | -------------------------------------------------------------------------------- /src/state/initialState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BASE_IMMIGRATION_RATE, 3 | RIGHT, 4 | STARTING_MORALE, 5 | VERSION, 6 | NEW_COLONISTS_PER_DAY, 7 | FOOD_PER_COLONIST, 8 | } from "../constants"; 9 | import { RawState } from "../types"; 10 | import { TutorialId } from "../types/TutorialId"; 11 | 12 | export function createInitialState({ 13 | completedTutorials, 14 | mapType, 15 | }: { 16 | completedTutorials: TutorialId[]; 17 | mapType: string; 18 | }) { 19 | const initialState: RawState = { 20 | version: VERSION, 21 | mapType: mapType ?? "standard", 22 | entities: {}, 23 | entitiesByPosition: {}, 24 | entitiesByComp: {}, 25 | messageLog: [], 26 | gameOver: false, 27 | victory: false, 28 | morale: STARTING_MORALE, 29 | time: { 30 | turn: 0, 31 | directionWeights: { n: 0, s: 0, e: 0, w: 0 }, 32 | }, 33 | laserState: "READY", 34 | resources: { 35 | METAL: 0, 36 | FOOD: NEW_COLONISTS_PER_DAY * FOOD_PER_COLONIST * 2, 37 | POWER: 0, 38 | MACHINERY: 0, 39 | }, 40 | resourceChanges: { 41 | METAL: [], 42 | FOOD: [], 43 | POWER: [], 44 | MACHINERY: [], 45 | }, 46 | resourceChangesThisTurn: { 47 | METAL: [], 48 | FOOD: [], 49 | POWER: [], 50 | MACHINERY: [], 51 | }, 52 | jobPriorities: { 53 | FARMS: 1, 54 | MINING_SPOTS: 2, 55 | MINES: 3, 56 | FACTORIES: 4, 57 | }, 58 | events: {}, 59 | lastAimingDirection: RIGHT, 60 | isAutoMoving: false, 61 | lastMoveWasFast: false, 62 | bordersKey: null, 63 | tutorials: { 64 | completed: completedTutorials, 65 | active: completedTutorials.includes(TutorialId.Basics) 66 | ? [] 67 | : [ 68 | { 69 | id: TutorialId.Basics, 70 | step: 0, 71 | }, 72 | ], 73 | }, 74 | }; 75 | 76 | return initialState; 77 | } 78 | -------------------------------------------------------------------------------- /src/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { getType } from "typesafe-actions"; 2 | import { Action, RawState } from "../types"; 3 | import actions from "./actions"; 4 | import { createInitialState } from "./initialState"; 5 | import { processTutorials } from "../lib/tutorials"; 6 | import wrapState from "./wrapState"; 7 | import Audio from "../lib/audio/Audio"; 8 | import Renderer from "../renderer/Renderer"; 9 | import DummyAudio from "../lib/audio/DummyAudio"; 10 | import { save } from "../lib/gameSave"; 11 | import Settings from "../types/Settings"; 12 | 13 | const GAME_OVER_ALLOW_LIST: string[] = [ 14 | getType(actions.newGame), 15 | getType(actions.undoTurn), 16 | getType(actions.continueVictory), 17 | ]; 18 | 19 | const AUTO_MOVE_ALLOW_LIST: string[] = [getType(actions.autoMove)]; 20 | 21 | export function makeReducer( 22 | renderer: Renderer, 23 | audio: Audio | DummyAudio, 24 | settings: Settings 25 | ) { 26 | return function reducer( 27 | state: RawState = createInitialState({ 28 | completedTutorials: [], 29 | mapType: "standard", 30 | }), 31 | action: Action 32 | ): RawState { 33 | const wrappedState = wrapState(state, renderer, audio, settings, save); 34 | 35 | if (state.gameOver && !GAME_OVER_ALLOW_LIST.includes(action.type)) { 36 | return state; 37 | } 38 | 39 | if (state.isAutoMoving && !AUTO_MOVE_ALLOW_LIST.includes(action.type)) { 40 | wrappedState.act.cancelAutoMove(); 41 | return wrappedState.raw; 42 | } 43 | 44 | wrappedState.handle(action); 45 | 46 | processTutorials( 47 | wrapState(state, renderer, audio, settings, () => {}), 48 | wrappedState, 49 | action 50 | ); 51 | 52 | return { ...wrappedState.raw, entities: { ...wrappedState.raw.entities } }; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/state/selectors/blueprintSelectors.ts: -------------------------------------------------------------------------------- 1 | import { RawState } from "../../types"; 2 | import { entitiesWithComps } from "./entitySelectors"; 3 | 4 | export function blueprint(state: RawState) { 5 | const entities = entitiesWithComps(state, "blueprint", "pos", "display"); 6 | if (entities.length) return entities[0]; 7 | return null; 8 | } 9 | 10 | export function hasActiveBlueprint(state: RawState): boolean { 11 | return Boolean(blueprint(state)); 12 | } 13 | -------------------------------------------------------------------------------- /src/state/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import * as entitySelectors from "./entitySelectors"; 2 | import * as miscSelectors from "./miscSelectors"; 3 | import * as blueprintSelectors from "./blueprintSelectors"; 4 | import * as statusSelectors from "./statusSelectors"; 5 | import * as tutorialSelectors from "./tutorialSelectors"; 6 | import { RawState } from "../../types"; 7 | 8 | export default { 9 | ...entitySelectors, 10 | ...miscSelectors, 11 | ...blueprintSelectors, 12 | ...statusSelectors, 13 | ...tutorialSelectors, 14 | state: (s: RawState) => s, 15 | }; 16 | -------------------------------------------------------------------------------- /src/state/selectors/miscSelectors.ts: -------------------------------------------------------------------------------- 1 | import { RawState } from "../../types"; 2 | 3 | export function nothing(state: RawState) { 4 | return null; 5 | } 6 | 7 | export function isEmpty(state: RawState) { 8 | return Object.keys(state.entities).length === 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/state/selectors/tutorialSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RawState } from "../../types"; 2 | import { TutorialId } from "../../types/TutorialId"; 3 | 4 | export function tutorialsState(state: RawState) { 5 | return state.tutorials; 6 | } 7 | 8 | export function completedTutorials(state: RawState) { 9 | return tutorialsState(state).completed; 10 | } 11 | 12 | export function activeTutorials(state: RawState) { 13 | return tutorialsState(state).active; 14 | } 15 | 16 | export function isTutorialCompleted(state: RawState, tutorial: TutorialId) { 17 | return completedTutorials(state).includes(tutorial); 18 | } 19 | 20 | export function isTutorialActive(state: RawState, tutorial: TutorialId) { 21 | return activeTutorials(state).some((t) => t.id === tutorial); 22 | } 23 | -------------------------------------------------------------------------------- /src/state/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { devToolsEnhancer } from "redux-devtools-extension/logOnlyInProduction"; 3 | import defaultSettings from "../data/defaultSettings"; 4 | import audio from "../lib/audio"; 5 | import renderer from "../renderer"; 6 | import { makeReducer } from "./reducer"; 7 | 8 | const reducer = makeReducer(renderer, audio, defaultSettings); 9 | const store = createStore(reducer, devToolsEnhancer({})); 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/state/systems/absorberSystem.ts: -------------------------------------------------------------------------------- 1 | import { Required } from "ts-toolbelt/out/Object/Required"; 2 | import { executeEffect } from "../../data/effects"; 3 | import { createEntityFromTemplate } from "../../lib/entities"; 4 | import { Entity } from "../../types"; 5 | import WrappedState from "../../types/WrappedState"; 6 | 7 | export default function absorberSystem(state: WrappedState): void { 8 | // fire absorbers 9 | state.select 10 | .entitiesWithComps("absorber") 11 | .filter((e) => e.absorber.aimingDirection) 12 | .forEach((entity) => state.act.fireWeapon({ source: entity.id })); 13 | 14 | // reset fired absorbers 15 | state.select 16 | .entitiesWithComps("absorber") 17 | .filter((e) => e.absorber.aimingDirection) 18 | .forEach((absorber) => { 19 | state.act.updateEntity({ 20 | id: absorber.id, 21 | absorber: { aimingDirection: null, charged: false }, 22 | }); 23 | executeEffect("CLEAR_UI_ABSORBER_CHARGE", state, undefined, absorber); 24 | }); 25 | 26 | // create charge indicators 27 | state.select.entitiesWithComps("pos", "absorber").forEach((entity) => { 28 | createChargeIndicatorIfNeeded(state, entity); 29 | }); 30 | } 31 | 32 | function createChargeIndicatorIfNeeded( 33 | state: WrappedState, 34 | entity: Required 35 | ) { 36 | if ( 37 | entity.absorber.charged && 38 | !state.select 39 | .entitiesAtPosition(entity.pos) 40 | .some((e) => e.template === "UI_ABSORBER_CHARGE") 41 | ) { 42 | state.act.addEntity( 43 | createEntityFromTemplate("UI_ABSORBER_CHARGE", { pos: entity.pos }) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/state/systems/absorberTriggerSystem.ts: -------------------------------------------------------------------------------- 1 | import { DOWN, LEFT, RIGHT, UP } from "../../constants"; 2 | import { getPositionToDirection } from "../../lib/geometry"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | export default function absorberTriggerSystem(state: WrappedState): void { 6 | for (const entity of state.select 7 | .entitiesWithComps("absorber", "pos") 8 | .filter((e) => e.absorber.charged)) { 9 | for (const direction of [UP, RIGHT, DOWN, LEFT]) { 10 | const pos = getPositionToDirection(entity.pos, direction); 11 | const entitiesAtPos = state.select.entitiesAtPosition(pos); 12 | if (entitiesAtPos.some((e) => e.ai && e.blocking?.lasers)) { 13 | state.act.targetWeapon({ direction, source: entity.id }); 14 | state.act.updateEntity({ 15 | id: entity.id, 16 | absorber: { 17 | ...entity.absorber, 18 | aimingDirection: direction, 19 | }, 20 | }); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/state/systems/aiSystem.ts: -------------------------------------------------------------------------------- 1 | import { Object } from "ts-toolbelt"; 2 | import { executePlan, clearPlan, makePlan } from "../../lib/ai"; 3 | import { Entity } from "../../types"; 4 | import WrappedState from "../../types/WrappedState"; 5 | 6 | export default function aiSystem(state: WrappedState): void { 7 | for (const entity of state.select.entitiesWithComps("ai", "pos")) { 8 | executePlan(state, entity); 9 | const refreshedEntity = state.select.entityById(entity.id); 10 | if (refreshedEntity && refreshedEntity.ai && refreshedEntity.pos) { 11 | clearPlan( 12 | state, 13 | refreshedEntity as Object.Required 14 | ); 15 | } 16 | } 17 | 18 | state.setRaw({ ...state.raw, movementCostCache: {} }); 19 | for (const entity of state.select.entitiesWithComps("ai", "pos")) { 20 | makePlan(state, entity); 21 | } 22 | state.setRaw({ ...state.raw, movementCostCache: {} }); 23 | } 24 | -------------------------------------------------------------------------------- /src/state/systems/aimingSystem.ts: -------------------------------------------------------------------------------- 1 | import { PLAYER_ID } from "../../constants"; 2 | import WrappedState from "../../types/WrappedState"; 3 | 4 | export default function aimingSystem(state: WrappedState) { 5 | if (state.select.isWeaponActive()) { 6 | state.act.targetWeapon({ 7 | direction: state.select.lastAimingDirection(), 8 | source: PLAYER_ID, 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/state/systems/animationToggleSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | 4 | export default function animationToggleSystem(state: WrappedState): void { 5 | state.select.entitiesWithComps("animationToggle").forEach((entity) => { 6 | if (areConditionsMet(state, entity, ...entity.animationToggle.conditions)) { 7 | state.renderer.playAnimation(entity.id); 8 | } else { 9 | state.renderer.stopAnimation(entity.id); 10 | } 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/state/systems/audioToggleSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | 4 | export default function audioToggleSystem(state: WrappedState): void { 5 | for (const entity of state.select.entitiesWithComps("audioToggle", "pos")) { 6 | const conditionsMet = areConditionsMet( 7 | state, 8 | entity, 9 | ...entity.audioToggle.conditions 10 | ); 11 | if (conditionsMet) { 12 | state.audio.loopAtPos( 13 | entity.audioToggle.soundName, 14 | entity.pos, 15 | entity.audioToggle.soundOptions 16 | ); 17 | } else { 18 | state.audio.stopAtPos(entity.audioToggle.soundName, entity.pos); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/state/systems/bordersSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | 3 | export default function bordersSystem(state: WrappedState): void { 4 | state.act.bordersUpdate(); 5 | } 6 | -------------------------------------------------------------------------------- /src/state/systems/buildingSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | 3 | export default function buildingSystem(state: WrappedState) { 4 | const blueprint = state.select.blueprint(); 5 | if (blueprint && blueprint.pos) { 6 | state.act.blueprintSelect({ 7 | template: blueprint.template, 8 | initialPos: blueprint.pos, 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/state/systems/colorToggleSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | 4 | export default function colorToggleSystem(state: WrappedState): void { 5 | for (const entity of state.select.entitiesWithComps( 6 | "colorToggle", 7 | "display" 8 | )) { 9 | const conditionsMet = areConditionsMet( 10 | state, 11 | entity, 12 | ...entity.colorToggle.conditions 13 | ); 14 | const newColor = conditionsMet 15 | ? entity.colorToggle.trueColor 16 | : entity.colorToggle.falseColor; 17 | if (newColor !== entity.display.color) { 18 | state.act.updateEntity({ 19 | id: entity.id, 20 | display: { 21 | ...entity.display, 22 | color: newColor, 23 | }, 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/state/systems/directionIndicationSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { getConstDir } from "../../lib/geometry"; 3 | import { UP, DOWN, LEFT, RIGHT } from "../../constants"; 4 | import { createEntityFromTemplate } from "../../lib/entities"; 5 | 6 | export default function directionIndicationSystem(state: WrappedState): void { 7 | state.act.removeEntities( 8 | state.select.entitiesWithComps("directionIndicator").map((e) => e.id) 9 | ); 10 | state.select.entitiesWithComps("pos", "ai").forEach((entity) => { 11 | const { pos } = entity; 12 | const direction = entity.ai.plannedActionDirection; 13 | if (direction) { 14 | if (getConstDir(direction) === UP) 15 | state.act.addEntity( 16 | createEntityFromTemplate("UI_DIRECTION_N", { pos }) 17 | ); 18 | if (getConstDir(direction) === DOWN) 19 | state.act.addEntity( 20 | createEntityFromTemplate("UI_DIRECTION_S", { pos }) 21 | ); 22 | if (getConstDir(direction) === LEFT) 23 | state.act.addEntity( 24 | createEntityFromTemplate("UI_DIRECTION_W", { pos }) 25 | ); 26 | if (getConstDir(direction) === RIGHT) 27 | state.act.addEntity( 28 | createEntityFromTemplate("UI_DIRECTION_E", { pos }) 29 | ); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/state/systems/emitterSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | 4 | export default function emitterSystem(state: WrappedState): void { 5 | state.select.entitiesWithComps("smokeEmitter", "pos").forEach((entity) => { 6 | entity.smokeEmitter.emitters.forEach((emitter) => { 7 | if (areConditionsMet(state, entity, ...emitter.conditions)) { 8 | state.renderer.addSmoke(entity.pos, emitter.offset); 9 | } else { 10 | state.renderer.stopSmoke(entity.pos, emitter.offset); 11 | } 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/state/systems/eventSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | 3 | export default function eventSystem(state: WrappedState): void { 4 | if (state.raw.events.COLONIST_DIED) { 5 | const count = state.raw.events.COLONIST_DIED; 6 | state.act.logMessage({ 7 | message: `${count} ${ 8 | count === 1 ? "colonist" : "colonists" 9 | } died, so you lost ${count} morale. Defend your colonists!`, 10 | type: "error", 11 | }); 12 | } 13 | 14 | state.setRaw({ 15 | ...state.raw, 16 | events: {}, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/state/systems/gameOverSystem.ts: -------------------------------------------------------------------------------- 1 | import { VICTORY_POPULATION, VICTORY_ON_TURN } from "../../constants"; 2 | import WrappedState from "../../types/WrappedState"; 3 | 4 | export default function gameOverSystem(state: WrappedState): void { 5 | if ( 6 | !state.select.player() || 7 | state.select.morale() <= 0 || 8 | state.select.population() === 0 9 | ) { 10 | state.setRaw({ 11 | ...state.raw, 12 | gameOver: true, 13 | victory: false, 14 | }); 15 | } else if (state.raw.time.turn >= VICTORY_ON_TURN && !state.raw.victory) { 16 | state.setRaw({ 17 | ...state.raw, 18 | gameOver: true, 19 | victory: true, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/state/systems/hungerSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { ResourceCode } from "../../data/resources"; 3 | import { FOOD_PER_COLONIST } from "../../constants"; 4 | 5 | export default function hungerSystem(state: WrappedState): void { 6 | if (state.select.turnOfNight() === 0) { 7 | const population = state.select.population(); 8 | const amountOfFoodNeeded = population * FOOD_PER_COLONIST; 9 | if (state.select.canAffordToPay(ResourceCode.Food, amountOfFoodNeeded)) { 10 | state.act.modifyResource({ 11 | resource: ResourceCode.Food, 12 | amount: -amountOfFoodNeeded, 13 | reason: "Colonists Eating", 14 | }); 15 | state.act.logMessage({ 16 | message: `Your ${population} ${ 17 | population === 1 ? "colonist" : "colonists" 18 | } ate ${ 19 | FOOD_PER_COLONIST * population 20 | } food. They won't eat again until tomorrow night.`, 21 | type: "info", 22 | }); 23 | } else { 24 | state.act.modifyResource({ 25 | resource: ResourceCode.Food, 26 | amount: -state.select.resource(ResourceCode.Food), 27 | reason: "Colonists Eating", 28 | }); 29 | state.act.reduceMorale({ amount: 1 }); 30 | state.act.logMessage({ 31 | message: `There was not enough food to go around, so your colony lost 1 morale. Each colonists needs ${FOOD_PER_COLONIST} food per night.`, 32 | type: "error", 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/state/systems/immigrationSystem.ts: -------------------------------------------------------------------------------- 1 | import { Pos } from "../../types"; 2 | import { 3 | BASE_IMMIGRATION_RATE, 4 | NEW_COLONISTS_PER_DAY, 5 | MAP_HEIGHT, 6 | MAP_WIDTH, 7 | } from "../../constants"; 8 | import WrappedState from "../../types/WrappedState"; 9 | import { createEntityFromTemplate } from "../../lib/entities"; 10 | import { getPositionsWithinRange } from "../../lib/geometry"; 11 | import { rangeTo } from "../../lib/math"; 12 | import { choose } from "../../lib/rng"; 13 | 14 | export default function immigrationSystem(state: WrappedState): void { 15 | if (state.select.isLastTurnOfNight()) { 16 | const player = state.select.player(); 17 | if (!player) { 18 | console.error("No player"); 19 | } else { 20 | const sourcePositions = [player.pos]; 21 | rangeTo(NEW_COLONISTS_PER_DAY).forEach(() => { 22 | const pos = findNewColonistPosition(state, sourcePositions); 23 | if (!pos) { 24 | console.error("no position for new immigrant found"); 25 | } else { 26 | state.act.addEntity(createEntityFromTemplate("COLONIST", { pos })); 27 | } 28 | }); 29 | state.act.logMessage({ 30 | message: `${NEW_COLONISTS_PER_DAY} new colonists have arrived!`, 31 | type: "info", 32 | }); 33 | } 34 | } 35 | } 36 | 37 | function findNewColonistPosition( 38 | state: WrappedState, 39 | sourcePositions: Pos[] 40 | ): Pos { 41 | const positions = sourcePositions 42 | .reduce((acc, pos) => { 43 | acc.push(...getPositionsWithinRange(pos, 3)); 44 | return acc; 45 | }, []) 46 | .filter((pos) => !state.select.isPositionBlocked(pos)) 47 | .filter( 48 | (pos) => 49 | pos.x >= 0 && pos.x < MAP_WIDTH && pos.y >= 0 && pos.y < MAP_HEIGHT 50 | ); 51 | return choose(positions); 52 | } 53 | -------------------------------------------------------------------------------- /src/state/systems/index.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import absorberSystem from "./absorberSystem"; 3 | import absorberTriggerSystem from "./absorberTriggerSystem"; 4 | import aimingSystem from "./aimingSystem"; 5 | import aiSystem from "./aiSystem"; 6 | import animationToggleSystem from "./animationToggleSystem"; 7 | import audioToggleSystem from "./audioToggleSystem"; 8 | import bordersSystem from "./bordersSystem"; 9 | import buildingSystem from "./buildingSystem"; 10 | import colonistsSystem from "./colonistsSystem"; 11 | import colorToggleSystem from "./colorToggleSystem"; 12 | import directionIndicationSystem from "./directionIndicationSystem"; 13 | import emitterSystem from "./emitterSystem"; 14 | import eventSystem from "./eventSystem"; 15 | import gameOverSystem from "./gameOverSystem"; 16 | import hungerSystem from "./hungerSystem"; 17 | import immigrationSystem from "./immigrationSystem"; 18 | import laserRechargingSystem from "./laserRechargingSystem"; 19 | import missingResourceIndicatorSystem from "./missingResourceIndicatorSystem"; 20 | import poweredSystem from "./poweredSystem"; 21 | import productionSystem from "./productionSystem"; 22 | import reflectorSystem from "./reflectorSystem"; 23 | import shieldSystem from "./shieldSystem"; 24 | import storageSystem from "./storageSystem"; 25 | import timeSystem from "./timeSystem"; 26 | import waveSystem from "./waveSystem"; 27 | import windowsSystem from "./windowsSystem"; 28 | 29 | export const turnEndSystems: ((state: WrappedState) => void)[] = [ 30 | absorberSystem, 31 | waveSystem, 32 | aiSystem, 33 | absorberTriggerSystem, 34 | productionSystem, 35 | immigrationSystem, 36 | colonistsSystem, 37 | hungerSystem, 38 | poweredSystem, 39 | shieldSystem, 40 | storageSystem, 41 | reflectorSystem, 42 | laserRechargingSystem, 43 | eventSystem, 44 | timeSystem, 45 | gameOverSystem, 46 | ]; 47 | 48 | export const cosmeticSystems: ((state: WrappedState) => void)[] = [ 49 | aimingSystem, 50 | buildingSystem, 51 | emitterSystem, 52 | bordersSystem, 53 | windowsSystem, 54 | directionIndicationSystem, 55 | missingResourceIndicatorSystem, 56 | colorToggleSystem, 57 | animationToggleSystem, 58 | audioToggleSystem, 59 | ]; 60 | -------------------------------------------------------------------------------- /src/state/systems/laserRechargingSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | 3 | export default function laserRechargingSystem(state: WrappedState): void { 4 | const laserState = state.select.laserState(); 5 | if (laserState === "FIRING") { 6 | state.setRaw({ 7 | ...state.raw, 8 | laserState: "RECHARGING", 9 | }); 10 | } else if (laserState === "RECHARGING") { 11 | state.setRaw({ 12 | ...state.raw, 13 | laserState: "READY", 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/state/systems/missingResourceIndicatorSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { Entity } from "../../types"; 3 | import { createEntityFromTemplate } from "../../lib/entities"; 4 | import { ColonistStatusCode } from "../../data/colonistStatuses"; 5 | import { ResourceCode } from "../../data/resources"; 6 | 7 | export default function missingResourceIndicatorSystem( 8 | state: WrappedState 9 | ): void { 10 | const missingResourceIndicators = state.select.entitiesWithComps( 11 | "missingResourceIndicator" 12 | ); 13 | state.act.removeEntities(missingResourceIndicators.map((e) => e.id)); 14 | 15 | const entitiesToAdd: Entity[] = []; 16 | const unpoweredEntities = state.select 17 | .entitiesWithComps("pos", "powered") 18 | .filter((e) => !e.powered.hasPower); 19 | entitiesToAdd.push( 20 | ...unpoweredEntities.map(({ pos }) => 21 | createEntityFromTemplate("UI_NO_POWER", { pos }) 22 | ) 23 | ); 24 | 25 | const colonistsWithMissingResources = state.select 26 | .colonists() 27 | .filter( 28 | ({ colonist }) => colonist.status === ColonistStatusCode.MissingResources 29 | ); 30 | colonistsWithMissingResources.forEach(({ colonist, pos }) => { 31 | colonist.missingResources.forEach((resource) => { 32 | if (resource === ResourceCode.Power) { 33 | entitiesToAdd.push(createEntityFromTemplate("UI_NO_POWER", { pos })); 34 | } else if (resource === ResourceCode.Metal) { 35 | entitiesToAdd.push(createEntityFromTemplate("UI_NO_METAL", { pos })); 36 | } else { 37 | console.error("Unhandled missing resource indicator:", resource); 38 | } 39 | }); 40 | }); 41 | 42 | entitiesToAdd.forEach((entity) => state.act.addEntity(entity)); 43 | } 44 | -------------------------------------------------------------------------------- /src/state/systems/poweredSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { ResourceCode } from "../../data/resources"; 3 | 4 | export default function poweredSystem(state: WrappedState): void { 5 | const poweredEntities = state.select.entitiesWithComps("powered"); 6 | poweredEntities.forEach((entity) => { 7 | if ( 8 | state.select.canAffordToPay( 9 | ResourceCode.Power, 10 | entity.powered.powerNeeded 11 | ) 12 | ) { 13 | state.act.modifyResource({ 14 | resource: ResourceCode.Power, 15 | amount: -entity.powered.powerNeeded, 16 | reason: entity.powered.resourceChangeReason, 17 | }); 18 | if (!entity.powered.hasPower) { 19 | state.act.updateEntity({ 20 | id: entity.id, 21 | powered: { 22 | ...entity.powered, 23 | hasPower: true, 24 | }, 25 | }); 26 | if (entity.pos) { 27 | state.audio.playAtPos("power_on", entity.pos); 28 | } 29 | } 30 | } else if (entity.powered.hasPower) { 31 | state.act.updateEntity({ 32 | id: entity.id, 33 | powered: { 34 | ...entity.powered, 35 | hasPower: false, 36 | }, 37 | }); 38 | if (entity.pos) { 39 | state.audio.playAtPos("power_off", entity.pos); 40 | } 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/state/systems/productionSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | 4 | export default function productionSystem(state: WrappedState): void { 5 | const producers = state.select.entitiesWithComps("production"); 6 | producers.forEach((entity) => { 7 | if (areConditionsMet(state, entity, ...entity.production.conditions)) { 8 | state.act.updateEntity({ 9 | id: entity.id, 10 | production: { 11 | ...entity.production, 12 | producedLastTurn: true, 13 | }, 14 | }); 15 | state.act.modifyResource({ 16 | resource: entity.production.resource, 17 | amount: entity.production.amount, 18 | reason: entity.production.resourceChangeReason, 19 | }); 20 | } else { 21 | state.act.updateEntity({ 22 | id: entity.id, 23 | production: { 24 | ...entity.production, 25 | producedLastTurn: false, 26 | }, 27 | }); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/state/systems/reflectorSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { areConditionsMet } from "../../lib/conditions"; 3 | import { getDistance } from "../../lib/geometry"; 4 | import colors from "../../colors"; 5 | 6 | export default function reflectorSystem(state: WrappedState): void { 7 | const reflectors = state.select.entitiesWithComps( 8 | "reflector", 9 | "pos", 10 | "display" 11 | ); 12 | const projectors = state.select.entitiesWithComps("projector", "pos"); 13 | for (const reflector of reflectors) { 14 | if (state.select.isPositionBlocked(reflector.pos)) { 15 | state.act.removeEntity(reflector.id); 16 | } 17 | 18 | const outOfRange = projectors 19 | .filter((projector) => 20 | areConditionsMet(state, projector, projector.projector.condition) 21 | ) 22 | .every( 23 | (projector) => 24 | getDistance(projector.pos, reflector.pos) > projector.projector.range 25 | ); 26 | if (outOfRange) { 27 | if (reflector.reflector.outOfRange) { 28 | state.act.removeEntity(reflector.id); 29 | } else { 30 | state.act.updateEntity({ 31 | id: reflector.id, 32 | reflector: { 33 | ...reflector.reflector, 34 | outOfRange: true, 35 | }, 36 | warning: { 37 | text: "Reflector out of range", 38 | }, 39 | display: { 40 | ...reflector.display, 41 | tile: ["blank", "blank", "reflector", "reflector"], 42 | color: colors.player, 43 | }, 44 | }); 45 | } 46 | } else if (reflector.reflector.outOfRange) { 47 | state.act.updateEntity({ 48 | id: reflector.id, 49 | reflector: { 50 | ...reflector.reflector, 51 | outOfRange: false, 52 | }, 53 | warning: undefined, 54 | display: { 55 | ...reflector.display, 56 | tile: "reflector", 57 | color: colors.player, 58 | }, 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/state/systems/shieldSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | 3 | export default function shieldSystem(state: WrappedState): void { 4 | state.select 5 | .entitiesWithComps("shieldGenerator", "powered") 6 | .filter((e) => !e.powered.hasPower) 7 | .map((e) => e.id) 8 | .forEach((id) => state.act.shieldDischarge(id)); 9 | state.select 10 | .entitiesWithComps("shieldGenerator") 11 | .map((e) => e.id) 12 | .forEach((id) => state.act.shieldCharge(id)); 13 | } 14 | -------------------------------------------------------------------------------- /src/state/systems/storageSystem.ts: -------------------------------------------------------------------------------- 1 | import WrappedState from "../../types/WrappedState"; 2 | import { ResourceCode } from "../../data/resources"; 3 | 4 | export default function storageSystem(state: WrappedState): void { 5 | const resources = state.select.resources(); 6 | const newResources = { ...resources }; 7 | (Object.entries(newResources) as [ResourceCode, number][]).forEach( 8 | ([resource, amount]) => { 9 | newResources[resource] = Math.min(amount, state.select.storage(resource)); 10 | } 11 | ); 12 | state.setRaw({ 13 | ...state.raw, 14 | resources: newResources, 15 | }); 16 | 17 | if (newResources.POWER < resources.POWER) { 18 | state.select 19 | .entitiesWithComps("temperature") 20 | .forEach((entity) => state.act.temperatureIncrease(entity.id)); 21 | } else { 22 | state.select 23 | .entitiesWithComps("temperature") 24 | .forEach((entity) => state.act.temperatureDecrease(entity.id)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/state/systems/timeSystem.ts: -------------------------------------------------------------------------------- 1 | import { TURNS_PER_DAY, TURNS_PER_NIGHT } from "../../constants"; 2 | import WrappedState from "../../types/WrappedState"; 3 | import { choose } from "../../lib/rng"; 4 | import colors from "../../colors"; 5 | 6 | export default function timeSystem(state: WrappedState): void { 7 | state.setRaw({ 8 | ...state.raw, 9 | time: { 10 | ...state.raw.time, 11 | turn: state.raw.time.turn + 1, 12 | }, 13 | }); 14 | if (state.select.turnOfDay() === 0) { 15 | state.setRaw({ 16 | ...state.raw, 17 | time: { 18 | ...state.raw.time, 19 | directionWeights: makeRandomDirectionWeights(), 20 | }, 21 | }); 22 | state.renderer.setBackgroundColor(colors.backgroundDay); 23 | state.audio.playMusic("day"); 24 | } 25 | 26 | if (state.select.turnOfNight() === 0) { 27 | state.renderer.setBackgroundColor(colors.backgroundNight); 28 | state.audio.playMusic("night"); 29 | } 30 | } 31 | 32 | function makeRandomDirectionWeights() { 33 | const weights = { 34 | n: 0, 35 | s: 0, 36 | e: 0, 37 | w: 0, 38 | }; 39 | for (let i = 0; i < 4; i++) { 40 | const choice: keyof typeof weights = choose(["n", "s", "e", "w"]); 41 | weights[choice] += 25; 42 | } 43 | return weights; 44 | } 45 | -------------------------------------------------------------------------------- /src/state/systems/windowsSystem.ts: -------------------------------------------------------------------------------- 1 | import { areConditionsMet } from "../../lib/conditions"; 2 | import { createEntityFromTemplate } from "../../lib/entities"; 3 | import WrappedState from "../../types/WrappedState"; 4 | 5 | export default function windowsSystem(state: WrappedState): void { 6 | state.select.entitiesWithComps("pos", "windowed").forEach((windowed) => { 7 | const existingWindow = state.select 8 | .entitiesAtPosition(windowed.pos) 9 | .find((e) => e.window); 10 | 11 | const newWindowTile = windowed.windowed.windowConditions.find( 12 | ({ condition }) => areConditionsMet(state, windowed, condition) 13 | )?.tile; 14 | 15 | if (existingWindow && !newWindowTile) { 16 | state.act.removeEntity(existingWindow.id); 17 | } else if ( 18 | existingWindow && 19 | newWindowTile && 20 | existingWindow.display && 21 | existingWindow.display.tile !== newWindowTile 22 | ) { 23 | state.act.updateEntity({ 24 | id: existingWindow.id, 25 | display: { 26 | ...existingWindow.display, 27 | tile: newWindowTile, 28 | }, 29 | }); 30 | } else if (!existingWindow && newWindowTile) { 31 | let window = createEntityFromTemplate("UI_WINDOW", { pos: windowed.pos }); 32 | if (window.display) { 33 | window = { 34 | ...window, 35 | display: { 36 | ...window.display, 37 | tile: newWindowTile, 38 | }, 39 | }; 40 | } 41 | state.act.addEntity(window); 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/state/wrapState.ts: -------------------------------------------------------------------------------- 1 | import Audio from "../lib/audio/Audio"; 2 | import DummyAudio from "../lib/audio/DummyAudio"; 3 | import Renderer from "../renderer/Renderer"; 4 | import { Action, RawState } from "../types"; 5 | import WrappedState from "../types/WrappedState"; 6 | import actions from "./actions"; 7 | import handleAction from "./handleAction"; 8 | import selectors from "./selectors"; 9 | import defaultRenderer from "../renderer"; 10 | import defaultAudio from "../lib/audio"; 11 | import { save as defaultSave } from "../lib/gameSave"; 12 | import Settings from "../types/Settings"; 13 | import defaultSettings from "../data/defaultSettings"; 14 | 15 | export default function wrapState( 16 | state: RawState, 17 | renderer: Renderer = defaultRenderer, 18 | audio: Audio | DummyAudio = defaultAudio, 19 | settings: Settings = defaultSettings, 20 | save: (state: RawState) => void = defaultSave 21 | ): WrappedState { 22 | const wrappedState: any = { 23 | raw: state, 24 | select: {}, 25 | act: {}, 26 | actions, 27 | renderer, 28 | audio, 29 | settings, 30 | save, 31 | }; 32 | wrappedState.setRaw = (newState: RawState) => { 33 | wrappedState.raw = newState; 34 | return wrappedState; 35 | }; 36 | wrappedState.handle = (action: Action) => { 37 | handleAction(wrappedState, action); 38 | return wrappedState; 39 | }; 40 | for (const [key, actionCreator] of Object.entries(actions)) { 41 | wrappedState.act[key] = (...args: any[]) => 42 | wrappedState.handle((actionCreator as any)(...args)); 43 | } 44 | for (const [key, selector] of Object.entries(selectors)) { 45 | wrappedState.select[key] = (...args: any[]) => 46 | (selector as any)(wrappedState.raw, ...args); 47 | } 48 | return wrappedState; 49 | } 50 | -------------------------------------------------------------------------------- /src/types/Action.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from "typesafe-actions"; 2 | import type actions from "../state/actions"; 3 | 4 | export type Action = ActionType; 5 | -------------------------------------------------------------------------------- /src/types/ConditionName.ts: -------------------------------------------------------------------------------- 1 | export type ConditionName = 2 | | "always" 3 | | "doesNotHaveTallNeighbors" 4 | | "hasOneActiveWorker" 5 | | "hasOneOrMoreColonists" 6 | | "hasThreeActiveWorkers" 7 | | "hasThreeOrMoreColonists" 8 | | "hasTwoActiveWorkers" 9 | | "hasTwoOrMoreColonists" 10 | | "isDay" 11 | | "isInBuildRange" 12 | | "isNotBlocked" 13 | | "isNotBuildingBlocked" 14 | | "isNotOnEdgeOfMap" 15 | | "isNotOnOtherBuilding" 16 | | "isOnFertile" 17 | | "isOnOre" 18 | | "isPowered" 19 | | "willNotHaveAdjacentShields"; 20 | -------------------------------------------------------------------------------- /src/types/ControlCode.ts: -------------------------------------------------------------------------------- 1 | export enum ControlCode { 2 | Up = "UP", 3 | Down = "DOWN", 4 | Left = "LEFT", 5 | Right = "RIGHT", 6 | 7 | Fire = "FIRE", 8 | 9 | Menu = "MENU", 10 | Menu1 = "MENU_1", 11 | Menu2 = "MENU_2", 12 | Menu3 = "MENU_3", 13 | Menu4 = "MENU_4", 14 | Menu5 = "MENU_5", 15 | Menu6 = "MENU_6", 16 | Menu7 = "MENU_7", 17 | Menu8 = "MENU_8", 18 | Menu9 = "MENU_9", 19 | Menu0 = "MENU_0", 20 | 21 | PlaceReflectorA = "PLACE_REFLECTOR_A", 22 | PlaceReflectorB = "PLACE_REFLECTOR_B", 23 | RemoveReflector = "REMOVE_REFLECTOR", 24 | ClearAllReflectors = "CLEAR_ALL_REFLECTORS", 25 | 26 | Rebuild = "REBUILD", 27 | RemoveBuilding = "REMOVE_BUILDING", 28 | RotateBuilding = "ROTATE_BUILDING", 29 | ToggleJobs = "TOGGLE_JOBS", 30 | Wait = "WAIT", 31 | QuickAction = "QUICK_ACTION", 32 | Back = "BACK", 33 | Undo = "UNDO", 34 | Help = "HELP", 35 | ZoomIn = "ZOOM_IN", 36 | ZoomOut = "ZOOM_OUT", 37 | Recenter = "RECENTER", 38 | Center = "CENTER", 39 | FocusJobPriorities = "FOCUS_JOB_PRIORITIES", 40 | DismissNotifications = "DISMISS_NOTIFICATIONS", 41 | 42 | FocusTutorials = "FOCUS_TUTORIALS", 43 | ToggleTutorials = "TOGGLE_TUTORIALS", 44 | DismissTutorial = "DISMISS_TUTORIAL", 45 | } 46 | -------------------------------------------------------------------------------- /src/types/Direction.ts: -------------------------------------------------------------------------------- 1 | export interface Direction { 2 | dx: number; 3 | dy: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/Effect.ts: -------------------------------------------------------------------------------- 1 | export type EffectId = 2 | | "CLEAR_BUILDING_FARM_GROWTH" 3 | | "CLEAR_UI_ABSORBER_CHARGE" 4 | | "CLEAR_UI_OVERHEATING_CRITICAL" 5 | | "CLEAR_UI_OVERHEATING_HOT" 6 | | "CLEAR_UI_OVERHEATING_VERY_HOT" 7 | | "CLEAR_UI_WINDOW" 8 | | "DESTROY" 9 | | "ON_COLONIST_DESTROYED" 10 | | "ON_FARM_WORKED" 11 | | "ON_ROAD_BUILD" 12 | | "RESET_WORK_CONTRIBUTED" 13 | | "SHIELD_DISCHARGE" 14 | | "SPAWN_BUILDING_WALL_CRACKED" 15 | | "SPAWN_BUILDING_WALL_CRUMBLING" 16 | | "SPAWN_ENEMY_DRONE" 17 | | "SPAWN_PLAYER_CORPSE" 18 | | "SPAWN_UI_OVERHEATING_CRITICAL" 19 | | "SPAWN_UI_OVERHEATING_HOT" 20 | | "SPAWN_UI_OVERHEATING_VERY_HOT"; 21 | 22 | export interface AllEffect { 23 | ALL: Effect[]; 24 | } 25 | 26 | export type Effect = EffectId | AllEffect; 27 | -------------------------------------------------------------------------------- /src/types/RawState.ts: -------------------------------------------------------------------------------- 1 | import type { JobTypeCode } from "../data/jobTypes"; 2 | import type { ResourceCode } from "../data/resources"; 3 | import type { Direction } from "./Direction"; 4 | import type { Entity, Pos } from "./Entity"; 5 | import type { TutorialId } from "./TutorialId"; 6 | 7 | export interface RawState { 8 | version: string; 9 | mapType: string; 10 | entities: Record; 11 | entitiesByPosition: Record>; 12 | entitiesByComp: Record>; 13 | messageLog: Record; 14 | gameOver: boolean; 15 | victory: boolean; 16 | morale: number; 17 | time: TimeState; 18 | laserState: "READY" | "ACTIVE" | "FIRING" | "RECHARGING"; 19 | resources: Record; 20 | resourceChanges: Record; 21 | resourceChangesThisTurn: Record< 22 | ResourceCode, 23 | { reason: string; amount: number }[] 24 | >; 25 | lastAimingDirection: Direction; 26 | jobPriorities: Record; 27 | isAutoMoving: boolean; 28 | events: Record; 29 | tutorials: TutorialsState; 30 | lastMoveWasFast: boolean; 31 | bordersKey: string | null; 32 | movementCostCache?: Record; 33 | } 34 | 35 | export interface TimeState { 36 | turn: number; 37 | directionWeights: { 38 | n: number; 39 | s: number; 40 | e: number; 41 | w: number; 42 | }; 43 | } 44 | 45 | export interface TutorialsState { 46 | completed: TutorialId[]; 47 | active: { 48 | id: TutorialId; 49 | step: number; 50 | }[]; 51 | } 52 | -------------------------------------------------------------------------------- /src/types/Settings.ts: -------------------------------------------------------------------------------- 1 | import type { ControlCode } from "./ControlCode"; 2 | 3 | export default interface Settings { 4 | aimInSameDirectionToFire: boolean; 5 | fireKeyActivatesAiming: boolean; 6 | unmodifiedAiming: boolean; 7 | unmodifiedBuilding: boolean; 8 | cursorModifierKey: "alt" | "ctrl" | "meta" | "shift"; 9 | keybindings: Record; 10 | musicVolume: number; 11 | sfxVolume: number; 12 | clickToMove: "ADJACENT" | "ALWAYS" | "NEVER"; 13 | } 14 | -------------------------------------------------------------------------------- /src/types/Tutorial.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from "./Action"; 2 | import type { RawState } from "./RawState"; 3 | import type { TutorialId } from "./TutorialId"; 4 | import type WrappedState from "./WrappedState"; 5 | 6 | export interface Tutorial { 7 | id: TutorialId; 8 | label: string; 9 | triggerSelector: (state: RawState) => boolean; 10 | steps: TutorialStep[]; 11 | } 12 | 13 | export interface TutorialStep { 14 | text: string; 15 | checkForCompletion: ( 16 | prevState: WrappedState, 17 | nextState: WrappedState, 18 | action: Action 19 | ) => boolean; 20 | elementHighlightSelectors?: string[]; 21 | isDismissible?: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /src/types/TutorialId.ts: -------------------------------------------------------------------------------- 1 | export enum TutorialId { 2 | Basics = "BASICS", 3 | Combat = "COMBAT", 4 | JobPriorities = "JOB_PRIORITIES", 5 | Morale = "MORALE", 6 | Residence = "RESIDENCE", 7 | Rotate = "ROTATE", 8 | } 9 | -------------------------------------------------------------------------------- /src/types/WrappedState.ts: -------------------------------------------------------------------------------- 1 | import type { Tuple, Object } from "ts-toolbelt"; 2 | import type { RawState } from "./RawState"; 3 | import type selectors from "../state/selectors"; 4 | import type actions from "../state/actions"; 5 | import type { Action } from "./Action"; 6 | import type { Entity } from "./Entity"; 7 | import Renderer from "../renderer/Renderer"; 8 | import Audio from "../lib/audio/Audio"; 9 | import DummyAudio from "../lib/audio/DummyAudio"; 10 | import Settings from "./Settings"; 11 | 12 | type SelectBase = { 13 | [K in keyof typeof selectors]: ( 14 | ...args: Tuple.Tail> 15 | ) => ReturnType; 16 | }; 17 | 18 | interface Select extends Omit { 19 | entitiesWithComps: ( 20 | ...comps: C[] 21 | ) => Object.Required[]; 22 | } 23 | 24 | type Act = { 25 | [K in keyof typeof actions]: ( 26 | ...args: Parameters 27 | ) => WrappedState; 28 | }; 29 | 30 | export default interface WrappedState { 31 | raw: RawState; 32 | setRaw: (state: RawState) => WrappedState; 33 | select: Select; 34 | act: Act; 35 | actions: typeof actions; 36 | handle: (action: Action) => WrappedState; 37 | renderer: Renderer; 38 | audio: Audio | DummyAudio; 39 | settings: Settings; 40 | save: (state: RawState) => void; 41 | } 42 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Entity"; 2 | export * from "./Action"; 3 | export * from "./RawState"; 4 | export * from "./Direction"; 5 | -------------------------------------------------------------------------------- /src/ui/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | import React, { ReactElement, useRef } from "react"; 3 | import { useDispatch, useSelector } from "./GameProvider"; 4 | import renderer from "../renderer"; 5 | import selectors from "../state/selectors"; 6 | import { Pos } from "../types"; 7 | import { getActionsAvailableAtPos, noFocusOnClick } from "../lib/controls"; 8 | import { LazyTippy } from "./LazyTippy"; 9 | 10 | interface Props { 11 | pos: Pos | null; 12 | onClose: () => void; 13 | children: ReactElement; 14 | } 15 | 16 | export default function ContextMenu({ pos, onClose, children }: Props) { 17 | const posRef = useRef(pos); 18 | if (pos) { 19 | posRef.current = pos; 20 | } 21 | 22 | return ( 23 | 29 | renderer.getClientRectFromPos(posRef.current || { x: 0, y: 0 }) 30 | } 31 | offset={[0, 0]} 32 | appendTo={document.body} 33 | content={} 34 | > 35 | {children} 36 | 37 | ); 38 | } 39 | 40 | function ContextMenuContent({ 41 | pos, 42 | onClose, 43 | }: { 44 | pos: Pos | null; 45 | onClose: () => void; 46 | }) { 47 | const dispatch = useDispatch(); 48 | const state = useSelector(selectors.state); 49 | if (!pos) return null; 50 | const actionControls = pos ? getActionsAvailableAtPos(state, pos) : []; 51 | return ( 52 |
53 |
    54 | {actionControls.length === 0 &&
  • No actions available
  • } 55 | {actionControls.map((a) => ( 56 |
  • 57 | 68 |
  • 69 | ))} 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/ui/EntityPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // @ts-ignore 3 | import tiles from "url:../assets/tiles/*.png"; 4 | import templates from "../data/templates"; 5 | import { createEntityFromTemplate } from "../lib/entities"; 6 | import colors from "../data/colors.json"; 7 | import { TemplateName } from "../types/TemplateName"; 8 | 9 | interface Props { 10 | templateName: TemplateName; 11 | style?: React.CSSProperties; 12 | } 13 | export default function EntityPreview({ templateName, style = {} }: Props) { 14 | if (!Object.keys(templates).includes(templateName)) return null; 15 | const entity = createEntityFromTemplate(templateName); 16 | if (!entity.display) return null; 17 | const maskImage = `url(${ 18 | tiles[ 19 | Array.isArray(entity.display.tile) 20 | ? entity.display.tile[0] 21 | : entity.display.tile 22 | ] 23 | })`; 24 | return ( 25 |
42 | {entity.description 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/ui/GameProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo, useReducer } from "react"; 2 | import { 3 | useDispatch as useReduxDispatch, 4 | useSelector as useReduxSelector, 5 | } from "react-redux"; 6 | import audio from "../lib/audio"; 7 | import renderer from "../renderer"; 8 | import { createInitialState } from "../state/initialState"; 9 | import { makeReducer } from "../state/reducer"; 10 | import { Action, RawState } from "../types"; 11 | import { useSettings } from "./SettingsProvider"; 12 | 13 | const initialState = createInitialState({ 14 | completedTutorials: [], 15 | mapType: "standard", 16 | }); 17 | 18 | const DispatchContext = React.createContext<(action: Action) => void>(() => {}); 19 | const StateContext = React.createContext(initialState); 20 | 21 | export default function GameProvider({ 22 | children, 23 | redux, 24 | }: { 25 | children: React.ReactNode; 26 | redux: boolean; 27 | }) { 28 | const [settings] = useSettings(); 29 | const reducer = useMemo( 30 | () => makeReducer(renderer, audio, settings), 31 | [settings] 32 | ); 33 | const [state, dispatch] = useReducer(reducer, initialState); 34 | const reduxDispatch = useReduxDispatch(); 35 | const reduxState = useReduxSelector(identity) as RawState; 36 | return ( 37 | 38 | 39 | {children} 40 | 41 | 42 | ); 43 | } 44 | 45 | function identity(value: T): T { 46 | return value; 47 | } 48 | 49 | export function useDispatch() { 50 | return useContext(DispatchContext); 51 | } 52 | 53 | export function useSelector(selector: (state: RawState) => T) { 54 | return selector(useContext(StateContext)); 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HEADER_CSS_WIDTH } from "../constants"; 3 | import { ControlCode } from "../types/ControlCode"; 4 | import { HotkeyGroup, useControl } from "./HotkeysProvider"; 5 | import Kbd from "./Kbd"; 6 | import { RouterPageProps } from "./Router"; 7 | import { useSettings } from "./SettingsProvider"; 8 | 9 | export default function Header({ navigateTo }: RouterPageProps) { 10 | const [settings] = useSettings(); 11 | const menuShortcuts = settings.keybindings[ControlCode.Menu]; 12 | 13 | useControl({ 14 | code: ControlCode.Menu, 15 | group: HotkeyGroup.Main, 16 | allowedGroups: [ 17 | HotkeyGroup.Intro, 18 | HotkeyGroup.GameOver, 19 | HotkeyGroup.Menu, 20 | HotkeyGroup.Tutorial, 21 | HotkeyGroup.JobPriorities, 22 | HotkeyGroup.BuildingSelection, 23 | ], 24 | callback: () => navigateTo("MainMenu"), 25 | }); 26 | 27 | return ( 28 |
29 |
30 |

Reflector: Laser Defense

31 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/HotkeyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import audio from "../lib/audio"; 3 | import { noFocusOnClick } from "../lib/controls"; 4 | import { ControlCode } from "../types/ControlCode"; 5 | import { HotkeyGroup, useControl } from "./HotkeysProvider"; 6 | import Kbd from "./Kbd"; 7 | import { useSettings } from "./SettingsProvider"; 8 | 9 | interface Props { 10 | label: string; 11 | className?: string; 12 | callback: () => void; 13 | controlCode: ControlCode; 14 | hotkeyGroup: HotkeyGroup; 15 | disabled?: boolean; 16 | disabledIsCosmeticOnly?: boolean; 17 | style?: React.CSSProperties; 18 | } 19 | export default function HotkeyButton({ 20 | label, 21 | className, 22 | callback, 23 | controlCode, 24 | hotkeyGroup, 25 | disabled, 26 | disabledIsCosmeticOnly, 27 | style, 28 | }: Props) { 29 | const wrappedCallback = () => { 30 | if (disabled) { 31 | audio.play("ui_unsuccessful_invalid"); 32 | } else { 33 | callback(); 34 | } 35 | }; 36 | 37 | useControl({ 38 | code: controlCode, 39 | group: hotkeyGroup, 40 | disabled: disabled && !disabledIsCosmeticOnly, 41 | callback: wrappedCallback, 42 | 43 | // set ctrl, alt, and meta to false, but leave shift undefined so hotkeys like "?" can still work 44 | ctrl: false, 45 | alt: false, 46 | meta: false, 47 | }); 48 | const [settings] = useSettings(); 49 | return ( 50 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/Introduction.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useBoolean } from "../hooks"; 3 | import selectors from "../state/selectors"; 4 | import { ControlCode } from "../types/ControlCode"; 5 | import { useSelector } from "./GameProvider"; 6 | import HotkeyButton from "./HotkeyButton"; 7 | import { HotkeyGroup, useControl } from "./HotkeysProvider"; 8 | import Kbd from "./Kbd"; 9 | import Modal from "./Modal"; 10 | 11 | export default function Introduction() { 12 | const [isOpen, open, close] = useBoolean(false); 13 | const turn = useSelector(selectors.turn); 14 | const player = useSelector(selectors.player); 15 | useEffect(() => { 16 | if (player && turn === 0) { 17 | open(); 18 | } 19 | }, [Boolean(player), turn]); 20 | 21 | useControl({ 22 | code: ControlCode.QuickAction, 23 | group: HotkeyGroup.Intro, 24 | callback: close, 25 | disabled: !isOpen, 26 | }); 27 | 28 | useControl({ 29 | code: ControlCode.Back, 30 | group: HotkeyGroup.Intro, 31 | callback: close, 32 | disabled: !isOpen, 33 | }); 34 | 35 | if (!isOpen) return null; 36 | 37 | return ( 38 | 39 |

Welcome to Reflector: Laser Defense

40 |

41 | You have been tasked with establishing a colony on an alien planet. 42 | Build your base, protect your colonists, and survive 10 days to win! 43 |

44 |

Good luck!

45 |

46 | Press ? to view full keyboard controls. 47 |

48 | 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/ui/Kbd.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | 3 | interface Props { 4 | className?: string; 5 | light?: boolean; 6 | noPad?: boolean; 7 | } 8 | 9 | export default function Kbd({ 10 | children, 11 | className, 12 | light, 13 | noPad, 14 | }: PropsWithChildren) { 15 | return ( 16 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/LazyTippy.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/destructuring-assignment */ 2 | 3 | // Will only render the `content` or `render` elements if the tippy is mounted to the DOM. 4 | // Replace with component and it should work the same. 5 | 6 | import React from "react"; 7 | import Tippy, { TippyProps } from "@tippyjs/react"; 8 | 9 | // Export own set of props (even if they are the same for now) to enable clients to be more future-proof 10 | export type LazyTippyProps = TippyProps; 11 | 12 | export const LazyTippy = (props: LazyTippyProps) => { 13 | const [mounted, setMounted] = React.useState(false); 14 | 15 | const lazyPlugin = { 16 | fn: () => ({ 17 | onMount: () => setMounted(true), 18 | onHidden: () => setMounted(false), 19 | }), 20 | }; 21 | 22 | const computedProps = { ...props }; 23 | 24 | computedProps.plugins = [lazyPlugin, ...(props.plugins || [])]; 25 | 26 | if (props.render) { 27 | const render = props.render; // let TypeScript safely derive that render is not undefined 28 | computedProps.render = (...args) => (mounted ? render(...args) : ""); 29 | } else { 30 | computedProps.content = mounted ? props.content : ""; 31 | } 32 | 33 | return ; 34 | }; 35 | -------------------------------------------------------------------------------- /src/ui/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | onClick: () => void; 5 | children: React.ReactNode; 6 | } 7 | 8 | export default function MenuButton({ children, onClick }: Props) { 9 | return ( 10 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/MenuSectionHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function MenuSectionHeader({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return

{children}

; 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/MenuSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useBoolean } from "../hooks"; 3 | import { ControlCode } from "../types/ControlCode"; 4 | import { HotkeyGroup, useControl } from "./HotkeysProvider"; 5 | 6 | interface Props { 7 | value: number; 8 | onChange: (value: number) => void; 9 | min: number; 10 | max: number; 11 | step: number; 12 | label: React.ReactNode; 13 | } 14 | 15 | export default function MenuSlider({ 16 | value, 17 | onChange, 18 | min, 19 | max, 20 | step, 21 | label, 22 | }: Props) { 23 | const [isFocused, focus, blur] = useBoolean(false); 24 | 25 | const increase = () => { 26 | onChange(Math.min(value + step, max)); 27 | }; 28 | 29 | const decrease = () => { 30 | onChange(Math.max(value - step, min)); 31 | }; 32 | 33 | useControl({ 34 | code: ControlCode.Right, 35 | group: HotkeyGroup.Menu, 36 | callback: increase, 37 | disabled: !isFocused, 38 | }); 39 | 40 | useControl({ 41 | code: ControlCode.Left, 42 | group: HotkeyGroup.Menu, 43 | callback: decrease, 44 | disabled: !isFocused, 45 | }); 46 | 47 | return ( 48 |
55 |
{label}
56 | onChange(parseFloat(e.target.value))} 64 | /> 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/ui/MenuTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ControlCode } from "../types/ControlCode"; 3 | import { HotkeyGroup, useControl } from "./HotkeysProvider"; 4 | import Icons from "./Icons"; 5 | 6 | export default function MenuTitle({ 7 | children, 8 | goBack, 9 | }: { 10 | children: React.ReactNode; 11 | goBack: () => void; 12 | }) { 13 | useControl({ 14 | code: ControlCode.Back, 15 | group: HotkeyGroup.Menu, 16 | callback: goBack, 17 | }); 18 | 19 | useControl({ 20 | code: ControlCode.Menu, 21 | group: HotkeyGroup.Menu, 22 | callback: goBack, 23 | }); 24 | 25 | return ( 26 |
27 |

28 | {children} 29 |
30 |

31 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/ui/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps, PropsWithChildren } from "react"; 2 | import ReactModal from "react-modal"; 3 | 4 | export default function Modal({ 5 | children, 6 | overlayClassName = "inset-0 fixed h-screen w-screen bg-opaqueWhite z-20", 7 | className = "w-2/5 h-auto mx-auto my-8 shadow-2xl bg-black p-8 border border-white border-solid rounded max-h-modal z-20 overflow-auto", 8 | ...rest 9 | }: PropsWithChildren>) { 10 | return ( 11 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/ResourceAmount.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ResourceCode } from "../data/resources"; 3 | import ResourceIcon from "./ResourceIcon"; 4 | 5 | interface Props { 6 | resourceCode: ResourceCode; 7 | amount: number; 8 | className?: string; 9 | } 10 | export default function ResourceAmount({ 11 | resourceCode, 12 | amount, 13 | className = "", 14 | }: Props) { 15 | return ( 16 | 17 | {amount} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/ResourceIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // @ts-ignore 3 | import tiles from "url:../assets/tiles/*.png"; 4 | import resources, { ResourceCode } from "../data/resources"; 5 | 6 | interface Props { 7 | resourceCode: ResourceCode; 8 | style?: React.CSSProperties; 9 | } 10 | export default function ResourceIcon({ resourceCode, style = {} }: Props) { 11 | const resource = resources[resourceCode]; 12 | const maskImage = `url(${tiles[resource.icon]})`; 13 | return ( 14 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/Router.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export interface RouterPageProps { 4 | navigateTo: (page: string) => void; 5 | goBack: () => void; 6 | } 7 | 8 | export default function Router({ 9 | pages, 10 | defaultPage, 11 | }: { 12 | pages: Record | undefined>; 13 | defaultPage: string; 14 | }) { 15 | const [stack, setStack] = useState([defaultPage]); 16 | const Page = pages[stack[0]]; 17 | 18 | const navigateTo = (page: string) => { 19 | if (page !== stack[0]) setStack([page, ...stack]); 20 | }; 21 | const goBack = () => { 22 | if (stack.length > 1) setStack(stack.slice(1)); 23 | }; 24 | const pageProps: RouterPageProps = { navigateTo, goBack }; 25 | 26 | if (Page) { 27 | return ; 28 | } else { 29 | console.error(`Unroutable page: ${stack[0]}`); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Alert({ className = "" }: { className?: string }) { 4 | return ( 5 |
9 | ! 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/pages/Credits.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Menu from "../Menu"; 3 | import MenuSectionHeader from "../MenuSectionHeader"; 4 | import MenuTitle from "../MenuTitle"; 5 | import { RouterPageProps } from "../Router"; 6 | 7 | export default function Credits({ goBack }: RouterPageProps) { 8 | return ( 9 | 10 | Credits 11 | Design, Programming, and Art 12 |
13 | Michael Moore{" | "} 14 | blog 15 | {" | "} 16 | @mmakesgames 17 |
18 |
19 | Some sprites adapted from{" "} 20 | Kenney 1-Bit Pack 21 |
22 | Music and Sound Design 23 |
24 | Leonardo Madau{" | "} 25 | website 26 |
27 | Playtesters 28 |
    29 |
  • aonemannnARMY
  • 30 |
  • Bbwunder
  • 31 |
  • Byte Arcane
  • 32 |
  • Connor Skehan
  • 33 |
  • EmperorCU
  • 34 |
  • 35 | Gornova | blog 36 |
  • 37 |
  • John Turk
  • 38 |
  • Kyzrati
  • 39 |
  • ozymoondias (Jonathon Moore)
  • 40 |
  • Quantumtroll
  • 41 |
  • Salmon
  • 42 |
43 |
44 | ); 45 | } 46 | 47 | function Link({ 48 | children, 49 | href, 50 | }: { 51 | children?: React.ReactNode; 52 | href: string; 53 | }) { 54 | return ( 55 | 61 | {children ?? href} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/ui/pages/NewGame.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RNG } from "rot-js"; 3 | import mapTypes from "../../data/mapTypes"; 4 | import actions from "../../state/actions"; 5 | import { useDispatch } from "../GameProvider"; 6 | import Menu from "../Menu"; 7 | import MenuButton from "../MenuButton"; 8 | import MenuOptionSelector from "../MenuOptionSelector"; 9 | import MenuTitle from "../MenuTitle"; 10 | import { RouterPageProps } from "../Router"; 11 | 12 | export default function NewGame({ goBack, navigateTo }: RouterPageProps) { 13 | const dispatch = useDispatch(); 14 | const [mapType, setMapType] = useState("standard"); 15 | 16 | return ( 17 |
18 | 19 | New Game 20 | ({ id: type, name: type }))} 33 | /> 34 | { 36 | dispatch( 37 | actions.newGame({ 38 | mapType: 39 | mapType === "random" 40 | ? RNG.getItem(Object.keys(mapTypes)) ?? "standard" 41 | : mapType, 42 | }) 43 | ); 44 | navigateTo("Game"); 45 | }} 46 | > 47 | Begin 48 | 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/ui/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "../GameProvider"; 3 | import audio from "../../lib/audio"; 4 | import actions from "../../state/actions"; 5 | import Menu from "../Menu"; 6 | import MenuButton from "../MenuButton"; 7 | import MenuSectionHeader from "../MenuSectionHeader"; 8 | import MenuSlider from "../MenuSlider"; 9 | import MenuTitle from "../MenuTitle"; 10 | import { RouterPageProps } from "../Router"; 11 | import { useSettings } from "../SettingsProvider"; 12 | import MenuOptionSelector from "../MenuOptionSelector"; 13 | 14 | export default function Settings({ navigateTo, goBack }: RouterPageProps) { 15 | const [settings, updateSettings] = useSettings(); 16 | const dispatch = useDispatch(); 17 | 18 | return ( 19 | 20 | Settings 21 | Controls 22 | navigateTo("Keybindings")}> 23 | View Keybindings 24 | 25 | 29 | updateSettings((prev) => ({ ...prev, clickToMove: value as any })) 30 | } 31 | options={[ 32 | { id: "ADJACENT", name: "Adjacent" }, 33 | { id: "ALWAYS", name: "Always" }, 34 | { id: "NEVER", name: "Never" }, 35 | ]} 36 | /> 37 | Volume 38 | 41 | updateSettings((prev) => ({ ...prev, musicVolume })) 42 | } 43 | min={0} 44 | max={100} 45 | step={10} 46 | label="Music" 47 | /> 48 | { 51 | updateSettings((prev) => ({ ...prev, sfxVolume })); 52 | audio.play("ui_successful_valid"); 53 | }} 54 | min={0} 55 | max={100} 56 | step={10} 57 | label="Effects" 58 | /> 59 | Miscellaneous 60 | dispatch(actions.resetTutorials())}> 61 | Reset Tutorials 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/ui/pages/index.ts: -------------------------------------------------------------------------------- 1 | import Credits from "./Credits"; 2 | import Game from "./Game"; 3 | import Keybindings from "./Keybindings"; 4 | import MainMenu from "./MainMenu"; 5 | import NewGame from "./NewGame"; 6 | import Settings from "./Settings"; 7 | 8 | export default { Credits, Game, Keybindings, MainMenu, NewGame, Settings }; 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("./src/data/colors.json"); 2 | 3 | module.exports = { 4 | content: ["./src/**/*.{html,ts,tsx}"], 5 | theme: { 6 | colors: { 7 | ...colors, 8 | }, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "strict": true, 6 | "target": "esnext", 7 | "module": "commonjs", 8 | "resolveJsonModule": true, 9 | "noEmit": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | --------------------------------------------------------------------------------