First cut of pulling out the relevant parts of my original game to make a little framework.

This commit is contained in:
Zed A. Shaw 2026-03-22 10:37:45 -04:00
commit 6a0c9e8d46
177 changed files with 18197 additions and 0 deletions

10
.gdbinit Normal file
View file

@ -0,0 +1,10 @@
set confirm off
set breakpoint pending on
set logging on
set logging overwrite on
set print pretty on
set pagination off
break abort
#break _invalid_parameter_noinfo
#break _invalid_parameter
catch throw

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# ---> Vim
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
subprojects
builddir
ttassets
backup
*.exe
*.dll
*.world
coverage
coverage/*
.venv
*.log
gdb.txt
releases
WORK
tmp

1
.vimrc_proj Normal file
View file

@ -0,0 +1 @@
set makeprg=make\ -f\ ../Makefile\ build

84
Makefile Normal file
View file

@ -0,0 +1,84 @@
ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
all: build
reset:
ifeq '$(OS)' 'Windows_NT'
powershell -executionpolicy bypass .\scripts\reset_build.ps1
else
sh -x ./scripts/reset_build.sh
endif
%.cpp : %.rl
ragel -I $(ROOT_DIR) -G1 -o $@ $<
%.dot: %.rl
ragel -Vp -I $(ROOT_DIR) -o $@ $<
%.png: %.dot
dot -Tpng $< -o $@
build:
meson compile -j 10 -C $(ROOT_DIR)/builddir
asset_build:
./builddir/icongen
release_build:
meson --wipe builddir -Db_ndebug=true --buildtype release
meson compile -j 10 -C builddir
debug_build:
meson setup --wipe builddir -Db_ndebug=true --buildtype debugoptimized
meson compile -j 10 -C builddir
tracy_build:
meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true
meson compile -j 10 -C builddir
test: build
./builddir/runtests -d yes
run: build test
ifeq '$(OS)' 'Windows_NT'
powershell "cp ./builddir/zedcaster.exe ."
./zedcaster
else
./builddir/zedcaster
endif
debug: build
gdb --nx -x .gdbinit --ex run --args builddir/runtests
debug_run: build
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/zedcaster
debug_walk: build test
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/zedcaster t
clean:
meson compile --clean -C builddir
debug_test: build
gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests
win_installer:
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp'
coverage_report:
powershell 'scripts/coverage_report.ps1'
money:
scc --exclude-dir subprojects --exclude-dir .git --exclude-dir wraps --exclude-dir scripts
arena:
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args ./builddir/arena
story:
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args ./builddir/storyboard
debug_animator:
gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args ./builddir/animator -s "rat_king_boss" -a "rat_king_boss" -b "test_background"
animator:
./builddir/animator.exe -s "rat_king_boss" -a "rat_king_boss" -b "test_background"

4
README.md Normal file
View file

@ -0,0 +1,4 @@
# A Game Engine?! But That's Illegal!
I took some of the code from my dungeon crawler game and turned into a rough engine so I could enter
the 2026 Dungeon Crawler game jam.

140
assets/ai.json Normal file
View file

@ -0,0 +1,140 @@
{
"profile": {
"enemy_found": 0,
"enemy_dead": 1,
"health_good": 2,
"no_more_items": 3,
"no_more_enemies": 4,
"in_combat": 5,
"have_item": 6,
"have_healing": 7,
"detect_enemy": 8,
"tough_personality": 9,
"cant_move": 10
},
"actions": [
{
"name": "find_enemy",
"cost": 5,
"needs": {
"detect_enemy": true,
"in_combat": false,
"no_more_enemies": false,
"enemy_found": false
},
"effects": {
"in_combat": true,
"enemy_found": true
}
},
{
"name": "run_away",
"cost": 0,
"needs": {
"tough_personality": false,
"in_combat": true,
"have_healing": false,
"health_good": false,
"cant_move": false
},
"effects": {
"in_combat": false
}
},
{
"name": "kill_enemy",
"cost": 10,
"needs": {
"no_more_enemies": false,
"in_combat": true,
"enemy_found": true,
"enemy_dead": false
},
"effects": {
"enemy_dead": true
}
},
{
"name": "collect_items",
"cost": 5,
"needs": {
"no_more_enemies": true,
"no_more_items": false
},
"effects": {
"no_more_items": true
}
},
{
"name": "find_healing",
"cost": 2,
"needs": {
"have_healing": false,
"in_combat": false,
"health_good": false
},
"effects": {
"health_good": true
}
},
{
"name": "use_healing",
"cost": 1,
"needs": {
"have_healing": true,
"health_good": false
},
"effects": {
"health_good": true
}
}
],
"states": {
"Host::initial_state": {
"enemy_found": false,
"enemy_dead": false,
"health_good": true,
"no_more_items": false,
"no_more_enemies": false,
"in_combat": false,
"have_item": false,
"have_healing": false,
"detect_enemy": true,
"tough_personality": true
},
"Host::final_state": {
"enemy_found": true,
"enemy_dead": true,
"health_good": true,
"no_more_items": true,
"in_combat": false,
"no_more_enemies": true
},
"Enemy::initial_state": {
"detect_enemy": false,
"tough_personality": true,
"enemy_found": false,
"enemy_dead": false,
"health_good": true,
"in_combat": false
},
"Enemy::final_state": {
"detect_enemy": true,
"enemy_found": true,
"enemy_dead": true,
"health_good": true
}
},
"scripts": {
"Host::actions":
["find_enemy",
"kill_enemy",
"collect_items",
"find_healing",
"run_away",
"use_healing"],
"Enemy::actions":
["find_enemy", "run_away", "kill_enemy", "use_healing"]
}
}

132
assets/animation.json Normal file
View file

@ -0,0 +1,132 @@
{
"burning_animation": {
"sheet": {
"frames": 3,
"frame_width": 256,
"frame_height": 256
},
"sequences": {
"idle": {"frames": [0,1,2,1,0], "durations": [5,5,5,5,5] }
},
"transforms": {
"basic": {
"min_x": 1.0,
"min_y": 1.0,
"max_x": 1.0,
"max_y": 1.0,
"flipped": false,
"scaled": true,
"toggled": false,
"looped": false,
"relative": true,
"easing": "none",
"motion": "move_none"
}
},
"forms": {
"idle": ["idle", "basic"]
},
"sounds": {
"idle": [],
"open": [],
"close": []
}
},
"lightning_animation": {
"sheet": {
"frames": 5,
"frame_width": 256,
"frame_height": 256
},
"sequences": {
"idle": {"frames": [0,1,2,3,4], "durations": [5,5,5,5,5] }
},
"transforms": {
"basic": {
"min_x": 1.0,
"min_y": 1.0,
"max_x": 1.0,
"max_y": 1.0,
"flipped": false,
"scaled": true,
"toggled": false,
"looped": false,
"relative": false,
"easing": "none",
"motion": "move_none"
}
},
"forms": {
"idle": ["idle", "basic"]
},
"sounds": {
"idle": [],
"open": [],
"close": []
}
},
"rat_with_sword": {
"sheet": {
"frames": 1,
"frame_width": 256,
"frame_height": 256
},
"sequences": {
"idle": {"frames": [0], "durations": [50] }
},
"transforms": {
"basic": {
"min_x": 1.0,
"min_y": 1.0,
"max_x": 1.22,
"max_y": 1.22,
"flipped": false,
"scaled": true,
"toggled": false,
"looped": false,
"relative": false,
"easing": "sine",
"motion": "scale_both"
}
},
"forms": {
"idle": ["idle", "basic"]
},
"sounds": {
"idle": [],
"open": [],
"close": []
}
},
"female_hand": {
"sheet": {
"frames": 3,
"frame_width": 900,
"frame_height": 600
},
"sequences": {
"idle": {"frames": [0, 1, 2], "durations": [10, 10, 20] }
},
"transforms": {
"basic": {
"min_x": 1.0,
"min_y": 1.0,
"max_x": 1.0,
"max_y": 1.0,
"flipped": false,
"scaled": false,
"toggled": true,
"looped": false,
"relative": false,
"easing": "none",
"motion": "move_none"
}
},
"forms": {
"idle": ["idle", "basic"]
},
"sounds": {
"idle": []
}
}
}

163
assets/cameras.json Normal file
View file

@ -0,0 +1,163 @@
{
"scene": {
"sheet": {
"frames": 1,
"frame_width": 1024,
"frame_height": 768
},
"sequences": {
"idle": {"frames": [0], "durations": [60] },
"pan": {"frames": [0], "durations": [60] },
"shake": {"frames": [0], "durations": [60] },
"dolly": {"frames": [0], "durations": [60] },
"bounce": {"frames": [0], "durations": [60] }
},
"transforms": {
"pan": {
"min_x": 0.0,
"min_y": 0.0,
"max_x": 0.0,
"max_y": 0.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": true,
"easing": "linear",
"relative": false,
"motion": "move_slide"
},
"shake": {
"min_x": -10.0,
"min_y": -10.0,
"max_x": 10.0,
"max_y": 10.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": true,
"relative": true,
"easing": "normal_dist",
"motion": "move_shake"
},
"dolly": {
"min_x": 0.8,
"min_y": 0.8,
"max_x": 1.0,
"max_y": 1.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": true,
"easing": "sine",
"relative": true,
"motion": "move_rush"
},
"bounce": {
"min_x": 0,
"min_y": -20,
"max_x": 0,
"max_y": 0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": true,
"relative": true,
"easing": "in_out_back",
"motion": "move_bounce"
}
},
"forms": {
"idle": ["idle", "idle"],
"pan": ["pan", "pan"],
"dolly": ["dolly", "dolly"],
"shake": ["shake", "shake"],
"bounce": ["bounce", "bounce"]
},
"sounds": {
"idle": [],
"pan": [],
"dolly": [],
"shake": [],
"bounce": []
}
},
"story": {
"sheet": {
"frames": 1,
"frame_width": 1024,
"frame_height": 768
},
"sequences": {},
"transforms": {
"pan": {
"min_x": 0.0,
"min_y": 0.0,
"max_x": 0.0,
"max_y": 0.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": false,
"easing": "linear",
"relative": false,
"motion": "move_slide"
},
"shake": {
"min_x": -10.0,
"min_y": -10.0,
"max_x": 10.0,
"max_y": 10.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": false,
"relative": true,
"easing": "normal_dist",
"motion": "move_shake"
},
"dolly": {
"min_x": 0.8,
"min_y": 0.8,
"max_x": 1.0,
"max_y": 1.0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": false,
"easing": "sine",
"relative": true,
"motion": "move_rush"
},
"bounce": {
"min_x": 0,
"min_y": -20,
"max_x": 0,
"max_y": 0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": false,
"relative": true,
"easing": "in_out_back",
"motion": "move_bounce"
},
"pause": {
"min_x": 0,
"min_y": 0,
"max_x": 0,
"max_y": 0,
"flipped": false,
"scaled": false,
"toggled": false,
"looped": false,
"relative": false,
"easing": "none",
"motion": "move_none"
}
},
"forms": {},
"sounds": {
"idle": []
}
}
}

117
assets/config.json Normal file
View file

@ -0,0 +1,117 @@
{
"sounds": {
"Sword_Hit_1": "assets/sounds/Creature_Sounds-Sword_Hit_1.ogg",
"Evil_Eye_Sound_1": "assets/sounds/Creature_Sounds-Evil_Eye_Sound_1.ogg",
"Evil_Eye_Sound_2": "assets/sounds/Creature_Sounds-Evil_Eye_Sound_2.ogg",
"Giant_Voice_1": "assets/sounds/Creature_Sounds-Giant_Voice_1.ogg",
"Medium_Rat": "assets/sounds/Creature_Sounds-Medium_Rat.ogg",
"Ranger_1": "assets/sounds/Creature_Sounds-Ranger_1.ogg",
"Small_Rat": "assets/sounds/Creature_Sounds-Small_Rat.ogg",
"Spider_1": "assets/sounds/Creature_Sounds-Spider_1.ogg",
"Spider_2": "assets/sounds/Creature_Sounds-Spider_2.ogg",
"Sword_Hit_1": "assets/sounds/Creature_Sounds-Sword_Hit_1.ogg",
"Sword_Hit_2": "assets/sounds/Creature_Sounds-Sword_Hit_2.ogg",
"walk": "assets/sounds/Creature_Sounds-Walk.ogg",
"Creature_Death_1": "assets/sounds/Creature_Sounds-Creature_Death_1.ogg",
"Humanoid_Death_1": "assets/sounds/Creature_Sounds-Humanoid_Death_1.ogg",
"Marmot_Scream_1": "assets/sounds/Creature_Sounds-Marmot_Scream_1.ogg",
"blank": "assets/sounds/blank.ogg",
"pickup": "assets/sounds/pickup.ogg",
"ambient_1": "assets/sounds/ambient_1.ogg",
"ui_click": "assets/sounds/ui_click.ogg",
"ui_hover": "assets/sounds/ui_hover.ogg",
"punch_cartoony": "assets/sounds/punch_cartoony.ogg",
"electric_shock_01": "assets/sounds/electric_shock_01.ogg",
"fireball_01": "assets/sounds/fireball_01.ogg",
"hp_status_80": "assets/sounds/hp_status_80.ogg",
"hp_status_60": "assets/sounds/hp_status_60.ogg",
"hp_status_30": "assets/sounds/hp_status_30.ogg",
"hp_status_10": "assets/sounds/hp_status_10.ogg",
"hp_status_00": "assets/sounds/hp_status_00.ogg"
},
"sprites": {
"rat_with_sword":
{"path": "assets/sprites/rat_with_sword.png",
"frame_width": 256,
"frame_height": 256
},
"torch_crappy":
{"path": "assets/items/torch_crappy.png",
"frame_width": 256,
"frame_height": 256
},
"torch_horizontal_floor":
{"path": "assets/items/torch_horizontal_floor.png",
"frame_width": 256,
"frame_height": 256
},
"peasant_girl":
{"path": "assets/sprites/peasant_girl_2.png",
"frame_width": 256,
"frame_height": 256
},
"healing_potion_small":
{"path": "assets/items/healing_potion_small.png",
"frame_width": 256,
"frame_height": 256
},
"well_down":
{"path": "assets/sprites/well_down.png",
"frame_width": 256,
"frame_height": 256
},
"dead_body":
{"path": "assets/sprites/dead_body.png",
"frame_width": 256,
"frame_height": 256
},
"door_plain":
{"path": "assets/doors/door_plain.png",
"frame_width": 256,
"frame_height": 256
},
"dead_body_lootable":
{"path": "assets/sprites/dead_body_lootable.png",
"frame_width": 256,
"frame_height": 256
},
"peasant_girl":
{"path": "assets/sprites/peasant_girl_2.png",
"frame_width": 256,
"frame_height": 256
},
"female_hand":
{"path": "assets/hands/female_hand.png",
"frame_width": 900,
"frame_height": 600
}
},
"worldgen": {
"enemy_probability": 50,
"device_probability": 10
},
"graphics": {
"smooth_textures": false
},
"compass": {
"N": 65514,
"NE": 8663,
"E": 8594,
"SE": 8600,
"S": 65516,
"SW": 8665,
"W": 8592,
"NW": 8598
},
"theme": {
"NOTE": "colors are in assets/palette.json",
"padding": 3,
"border_px": 1,
"text_size": 20,
"label_size": 20,
"font_file_name": "assets/text.otf"
},
"player": {
"hands": "female_hand"
}
}

46
assets/devices.json Normal file
View file

@ -0,0 +1,46 @@
{
"STAIRS_DOWN": {
"id": "STAIRS_DOWN",
"name": "Stairs Down",
"placement": "fixed",
"description": "Stairs that go down further into the dungeon.",
"inventory_count": 0,
"randomized": false,
"components": [
{"_type": "Tile", "display": 6105,
"foreground": "devices/fg:stairs_down",
"background": "devices/bg:stairs_down"
},
{"_type": "Device",
"config": {},
"events": ["STAIRS_DOWN"]},
{"_type": "Sprite", "name": "well_down", "width": 256, "height": 256, "scale": 1.0}
]
},
"DEAD_BODY_LOOTABLE": {
"id": "DEAD_BODY_LOOTABLE",
"name": "Grave Stone",
"description": "Something died here. Was this your doing?",
"components": [
{"_type": "Tile", "display": 1890,
"foreground": "devices/fg:dead_body_lootable",
"background": "devices/bg:dead_body_lootable"
},
{"_type": "Device", "config": {}, "events": ["LOOT_CONTAINER"]},
{"_type": "Sprite", "name": "dead_body_lootable", "width": 256, "height": 256, "scale": 1.0},
{"_type": "Sound", "attack": "pickup", "death": "blank"}
]
},
"DEAD_BODY": {
"id": "DEAD_BODY",
"name": "Something Dead",
"description": "You can't loot this, weirdo.",
"components": [
{"_type": "Tile", "display": 1939,
"foreground": "devices/fg:dead_body",
"background": "devices/bg:dead_body"
},
{"_type": "Sprite", "name": "dead_body", "width": 256, "height": 256, "scale": 1.0}
]
}
}

32
assets/enemies.json Normal file
View file

@ -0,0 +1,32 @@
{
"PLAYER_TILE": {
"placement": "fixed",
"components": [
{"_type": "Tile", "display": 10733,
"foreground": "enemies/fg:player",
"background": "color:transparent"
},
{"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 50, "dead": false},
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
{"_type": "Collision", "has": true},
{"_type": "EnemyConfig", "ai_script": "Host::actions", "ai_start_name": "Host::initial_state", "ai_goal_name": "Host::final_state"},
{"_type": "Personality", "hearing_distance": 5, "tough": false},
{"_type": "LightSource", "strength": 35, "radius": 2.0}
]
},
"RAT_GIANT": {
"components": [
{"_type": "Tile", "display": 2220,
"foreground": "enemies/fg:rat_giant",
"background": "color:transparent"
},
{"_type": "Combat", "hp": 50, "max_hp": 50, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 2, "dead": false},
{"_type": "Collision", "has": true},
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
{"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
{"_type": "Personality", "hearing_distance": 5, "tough": false},
{"_type": "Sprite", "name": "rat_with_sword", "scale": 1.0},
{"_type": "Sound", "attack": "Small_Rat", "death": "Creature_Death_1"}
]
}
}

12
assets/icons.json Normal file
View file

@ -0,0 +1,12 @@
{
"healing_potion_small":
{"path": "assets/icons/healing_potion_small.png",
"frame_width": 96,
"frame_height": 96
},
"torch_horizontal_floor":
{"path": "assets/icons/torch_horizontal_floor.png",
"frame_width": 96,
"frame_height": 96
}
}

32
assets/items.json Normal file
View file

@ -0,0 +1,32 @@
{
"TORCH_BAD": {
"id": "TORCH_BAD",
"name": "Crappy Torch",
"description": "A torch that barely lights the way. You wonder if it'd be better to not see the person who murders you.",
"inventory_count": 1,
"components": [
{"_type": "LightSource", "strength": 50, "radius": 2.5},
{"_type": "Tile", "display": 3848,
"foreground": "items/fg:flame",
"background": "color:transparent"
},
{"_type": "Sprite", "name": "torch_horizontal_floor", "width": 256, "height": 256, "scale": 1.0},
{"_type": "Sound", "attack": "pickup", "death": "blank"}
]
},
"POTION_HEALING_SMALL": {
"id": "POTION_HEALING_SMALL",
"name": "Small Healing Potion",
"description": "A small healing potion.",
"inventory_count": 1,
"components": [
{"_type": "Tile", "display": 1003,
"foreground": "items/fg:potion",
"background": "color:transparent"
},
{"_type": "Curative", "hp": 20},
{"_type": "Sprite", "name": "healing_potion_small", "width": 256, "height": 256, "scale": 1.0},
{"_type": "Sound", "attack": "pickup", "death": "blank"}
]
}
}

146
assets/map_tiles.json Normal file
View file

@ -0,0 +1,146 @@
[
{
"centered": false,
"display": 35,
"x": 0,
"y": 0
},
{
"centered": false,
"display": 8284,
"x": 64,
"y": 0
},
{
"centered": false,
"display": 11590,
"x": 128,
"y": 0
},
{
"centered": false,
"display": 10899,
"x": 192,
"y": 0
},
{
"centered": false,
"display": 9256,
"x": 256,
"y": 0
},
{
"centered": false,
"display": 9608,
"x": 320,
"y": 0
},
{
"centered": false,
"display": 10747,
"x": 384,
"y": 0
},
{
"centered": false,
"display": 8285,
"x": 448,
"y": 0
},
{
"centered": true,
"display": 1003,
"x": 512,
"y": 0
},
{
"centered": true,
"display": 3848,
"x": 576,
"y": 0
},
{
"centered": true,
"display": 85,
"x": 0,
"y": 64
},
{
"centered": true,
"display": 1939,
"x": 64,
"y": 64
},
{
"centered": true,
"display": 1890,
"x": 128,
"y": 64
},
{
"centered": true,
"display": 8687,
"x": 192,
"y": 64
},
{
"centered": true,
"display": 6105,
"x": 256,
"y": 64
},
{
"centered": true,
"display": 8793,
"x": 320,
"y": 64
},
{
"centered": true,
"display": 95,
"x": 384,
"y": 64
},
{
"centered": true,
"display": 1898,
"x": 448,
"y": 64
},
{
"centered": true,
"display": 42586,
"x": 512,
"y": 64
},
{
"centered": true,
"display": 2216,
"x": 576,
"y": 64
},
{
"centered": true,
"display": 10733,
"x": 0,
"y": 128
},
{
"centered": true,
"display": 2220,
"x": 64,
"y": 128
},
{
"centered": true,
"display": 1218,
"x": 128,
"y": 128
},
{
"centered": true,
"display": 1087,
"x": 128,
"y": 128
}
]

61
assets/palette.json Normal file
View file

@ -0,0 +1,61 @@
{
"color": {
"transparent": [100, 100, 100, 100],
"BAD": [255, 0, 0]
},
"gui/theme": {
"black": [0, 0, 0, 255],
"dark_dark": [10, 10, 10, 255],
"dark_mid": [30, 30, 30, 255],
"dark_light": [60, 60, 60, 255],
"mid": [100, 100, 100, 255],
"light_dark": [150, 150, 150, 255],
"light_mid": [200, 200, 200, 255],
"light_light": [230, 230, 230, 255],
"white": [255, 255, 255, 255],
"fill_color": "gui/theme:dark_mid",
"text_color": "gui/theme:light_light",
"bg_color": "gui/theme:mid",
"border_color": "gui/theme:dark_dark",
"bg_color_dark": "gui/theme:black"
},
"map/theme": {
"black": [0, 0, 0, 255],
"dark_dark": [10, 10, 10, 255],
"dark_mid": [30, 30, 30, 255],
"dark_light": [60, 60, 60, 255],
"mid": [100, 100, 100, 255],
"light_dark": [150, 150, 150, 255],
"light_mid": [200, 200, 200, 255],
"light_light": [230, 230, 230, 255],
"white": [255, 255, 255, 255]
},
"items/fg": {
"flame": "map/theme:white",
"potion": "map/theme:white"
},
"enemies/fg": {
"player": "map/theme:white",
"rat_giant": "map/theme:white"
},
"tiles/fg": {
"floor_tile": "map/theme:mid",
"wall_plain": "map/theme:dark_mid",
"ceiling_black": "color:transparent"
},
"tiles/bg": {
"floor_tile": "map/theme:dark_dark",
"wall_plain": "map/theme:dark_dark",
"ceiling_black": "color:transparent"
},
"devices/fg": {
"stairs_down": [24, 205, 189],
"dead_body": [32, 123, 164],
"dead_body_lootable": [32, 123, 164]
},
"devices/bg": {
"stairs_down": [24, 205, 189],
"dead_body": [24, 205, 189],
"dead_body_lootable": [24, 205, 189]
}
}

7
assets/room_themes.json Normal file
View file

@ -0,0 +1,7 @@
[
{
"name": "Plain",
"floor": "floor_tile",
"walls": "wall_plain"
}
]

26
assets/shaders.json Normal file
View file

@ -0,0 +1,26 @@
{
"ui_shader": {
"file_name": "assets/shaders/ui_shader.frag",
"type": "fragment"
},
"ERROR": {
"file_name": "assets/shaders/ui_error.frag",
"type": "fragment"
},
"rayview_sprites": {
"file_name": "assets/shaders/rayview_sprites.frag",
"type": "fragment"
},
"flame": {
"file_name": "assets/shaders/flame_trash.frag",
"type": "fragment"
},
"lightning": {
"file_name": "assets/shaders/lightning_attack.frag",
"type": "fragment"
},
"boss_hit": {
"file_name": "assets/shaders/flame_trash.frag",
"type": "fragment"
}
}

View file

@ -0,0 +1,79 @@
#version 120
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D source;
uniform float u_mouse;
uniform float value = 0.2;
uniform int octaves=8;
float random (in vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))*
43758.5453123);
}
float noise(in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) +
(c - a) * u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
float fbm(in vec2 st) {
float v = 0.0;
float a = 0.5;
vec2 shift = vec2(100.0);
mat2 rot = mat2(cos(0.5), sin(0.5),
-sin(0.5), cos(0.5));
for(int i = 0; i < octaves; i++) {
v += a * noise(st);
st = rot * st * 2.0 + shift;
a *= 0.5;
}
return v;
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy * 3.0;
vec3 color = vec3(0.0);
float speed = u_time * 10.0;
float value = 0.8; // cos(u_time) * cos(u_time);
vec2 q = vec2(0.0);
q.x = fbm(st + 0.00 * speed);
q.y = fbm(st + vec2(1.0));
vec2 r = vec2(0,0);
r.x += fbm( st + 1.0*q + vec2(1.0, 0.0)+ 0.15* speed );
r.y += fbm( st + 1.0*q + vec2(-1.0, 0.0)+ 0.126* speed);
float f = fbm(st * r);
color = mix(vec3(0.666667,0.619608, 0.122777),
vec3(0.666667,0.666667,0.498039),
clamp((f*f)*4.0,0.0,1.0));
color = mix(color,
vec3(0.666667, 0.122222, 0.0666667),
clamp(length(r.x), 0.0, 1.0));
color *= (f*f*f+0.5*f*f+0.6*f) * value;
vec4 pixel = texture2D(source, gl_TexCoord[0].xy);
float mask = color.r * pixel.a;
gl_FragColor = gl_Color * vec4(color, mask) + pixel;
}

View file

@ -0,0 +1,79 @@
#version 120
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D source;
uniform float u_mouse;
uniform float value = 0.2;
uniform int octaves=8;
float random (in vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))*
43758.5453123);
}
float noise(in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) +
(c - a) * u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
float fbm(in vec2 st) {
float v = 0.0;
float a = 0.5;
vec2 shift = vec2(100.0);
mat2 rot = mat2(cos(0.5), sin(0.5),
-sin(0.5), cos(0.5));
for(int i = 0; i < octaves; i++) {
v += a * noise(st);
st = rot * st * 2.0 + shift;
a *= 0.5;
}
return v;
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy * 3.0;
vec3 color = vec3(0.0);
float speed = u_time * 40.0;
float value = cos(u_time) * cos(u_time);
vec2 q = vec2(0.0);
q.x = fbm(st + 0.00 * speed);
q.y = fbm(st + vec2(1.0));
vec2 r = vec2(0,0);
r.x += fbm( st + 1.0*q + vec2(1.0, 0.0)+ 0.15* speed );
r.y += fbm( st + 1.0*q + vec2(-1.0, 0.0)+ 0.126* speed);
float f = fbm(st / r);
color = mix(vec3(0.122777,0.619608, 0.666667),
vec3(0.498039,0.666667,0.666667),
clamp((f*f)*4.0,0.0,1.0));
color = mix(color,
vec3(0.0666667, 0.122222, 0.666667),
clamp(length(r.x), 0.0, 1.0));
color *= (f*f*f+0.5*f*f+0.6*f) * value;
vec4 pixel = texture2D(source, gl_TexCoord[0].xy);
float mask = color.r * pixel.a;
gl_FragColor = gl_Color * vec4(color, mask) + pixel;
}

View file

@ -0,0 +1,25 @@
uniform sampler2D source;
uniform sampler2D bloom;
uniform vec2 offsetFactor;
uniform float darkness;
void main()
{
vec2 textureCoordinates = gl_TexCoord[0].xy;
vec4 color = vec4(0.0);
color += texture2D(source, textureCoordinates - 4.0 * offsetFactor) * 0.0162162162;
color += texture2D(source, textureCoordinates - 3.0 * offsetFactor) * 0.0540540541;
color += texture2D(source, textureCoordinates - 2.0 * offsetFactor) * 0.1216216216;
color += texture2D(source, textureCoordinates - offsetFactor) * 0.1945945946;
color += texture2D(source, textureCoordinates) * 0.2270270270;
color += texture2D(source, textureCoordinates + offsetFactor) * 0.1945945946;
color += texture2D(source, textureCoordinates + 2.0 * offsetFactor) * 0.1216216216;
color += texture2D(source, textureCoordinates + 3.0 * offsetFactor) * 0.0540540541;
color += texture2D(source, textureCoordinates + 4.0 * offsetFactor) * 0.0162162162;
vec4 sourceFragment = texture2D(source, gl_TexCoord[0].xy);
vec4 bloomFragment = texture2D(bloom, gl_TexCoord[0].xy);
float alpha = color.a;
gl_FragColor = (color + sourceFragment - bloomFragment) * darkness;
gl_FragColor.a = alpha;
}

View file

@ -0,0 +1,18 @@
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_duration;
uniform float u_time;
uniform float u_time_end;
uniform sampler2D texture;
uniform bool is_shape;
void main() {
if(is_shape) {
vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = gl_Color * color;
} else {
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = gl_Color * color * pixel;
}
}

View file

@ -0,0 +1,29 @@
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_duration;
uniform float u_time;
uniform float u_time_end;
uniform sampler2D texture;
uniform bool is_shape;
uniform bool hover;
vec4 blink() {
if(hover) {
return vec4(0.95, 0.95, 1.0, 1.0);
} else {
float tick = (u_time_end - u_time) / u_duration;
float blink = mix(0.5, 1.0, tick);
return vec4(blink, blink, blink, 1.0);
}
}
void main() {
vec4 color = blink();
if(!is_shape) {
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
color *= pixel;
}
gl_FragColor = gl_Color * color;
}

View file

@ -0,0 +1,12 @@
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_duration;
uniform float u_time;
uniform float u_time_end;
void main() {
float tick = (u_time_end - u_time) / u_duration;
float blink = smoothstep(1.0, 0.5, tick);
vec4 color = vec4(blink, blink, blink, 1.0);
gl_FragColor = gl_Color * color;
}

36
assets/tiles.json Normal file
View file

@ -0,0 +1,36 @@
{
"floor_tile": {
"texture": "assets/textures/floor_gray_stone.png",
"display": 8284,
"ceiling": "ceiling_black",
"light": 0,
"foreground": "tiles/fg:floor_tile",
"background": "tiles/bg:floor_tile",
"id": 0
},
"wall_plain": {
"texture": "assets/textures/wall_plain.png",
"display": 9608,
"light": 0,
"door": "door_plain",
"foreground": "tiles/fg:wall_plain",
"background": "tiles/bg:wall_plain",
"id": 1
},
"ceiling_black": {
"texture": "assets/textures/ceiling_black.png",
"display": 35,
"light": 0,
"foreground": "tiles/fg:ceiling_black",
"background": "tiles/bg:ceiling_black",
"id": 2
},
"door_plain": {
"texture": "assets/doors/door_plain.png",
"display": 1087,
"light": 10,
"foreground": "tiles/fg:wall_plain",
"background": "tiles/bg:wall_plain",
"id": 8
}
}

142
meson.build Normal file
View file

@ -0,0 +1,142 @@
project('raycaster', 'cpp',
version: '0.1.0',
default_options: [
'cpp_std=c++23',
'cpp_args=-D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1',
])
# use this for common options only for our executables
cpp_args=[
'-Wno-unused-parameter',
'-Wno-unused-function',
'-Wno-unused-variable',
'-Wno-unused-but-set-variable',
'-Wno-deprecated-declarations',
]
link_args=[]
# these are passed as override_defaults
exe_defaults = [ 'warning_level=2' ]
cc = meson.get_compiler('cpp')
dependencies = []
if build_machine.system() == 'windows'
add_global_link_arguments(
'-static-libgcc',
'-static-libstdc++',
'-static',
'-flto',
language: 'cpp',
)
sfml_main = subproject('sfml').get_variable('sfml_main_dep')
opengl32 = cc.find_library('opengl32', required: true)
winmm = cc.find_library('winmm', required: true)
gdi32 = cc.find_library('gdi32', required: true)
dependencies += [
opengl32, winmm, gdi32, sfml_main
]
exe_defaults += ['werror=true']
elif build_machine.system() == 'darwin'
add_global_link_arguments(
language: 'cpp',
)
opengl = dependency('OpenGL')
corefoundation = dependency('CoreFoundation')
carbon = dependency('Carbon')
cocoa = dependency('Cocoa')
iokit = dependency('IOKit')
corevideo = dependency('CoreVideo')
link_args += ['-ObjC']
exe_defaults += ['werror=false']
dependencies += [
opengl, corefoundation, carbon, cocoa, iokit, corevideo
]
endif
catch2 = subproject('catch2').get_variable('catch2_with_main_dep')
fmt = subproject('fmt').get_variable('fmt_dep')
json = subproject('nlohmann_json').get_variable('nlohmann_json_dep')
freetype2 = subproject('freetype2').get_variable('freetype_dep')
flac = subproject('flac').get_variable('flac_dep')
ogg = subproject('ogg').get_variable('libogg_dep')
vorbis = subproject('vorbis').get_variable('vorbis_dep')
vorbisfile = subproject('vorbis').get_variable('vorbisfile_dep')
vorbisenc = subproject('vorbis').get_variable('vorbisenc_dep')
sfml_audio = subproject('sfml').get_variable('sfml_audio_dep')
sfml_graphics = subproject('sfml').get_variable('sfml_graphics_dep')
sfml_network = subproject('sfml').get_variable('sfml_network_dep')
sfml_system = subproject('sfml').get_variable('sfml_system_dep')
sfml_window = subproject('sfml').get_variable('sfml_window_dep')
lel_guecs = subproject('lel-guecs').get_variable('lel_guecs_dep')
inc_dirs = include_directories('src')
subdir('src')
subdir('tests')
dependencies += [
fmt, json, freetype2,
flac, ogg, vorbis, vorbisfile, vorbisenc,
sfml_audio, sfml_graphics,
sfml_network, sfml_system,
sfml_window, lel_guecs
]
caster_lib = static_library('caster',
sources,
cpp_args: cpp_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies)
caster_dep = declare_dependency(
link_with: caster_lib,
include_directories: inc_dirs)
dependencies += [ caster_dep ]
executable('runtests', sources + tests,
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies + [catch2])
executable('zedcaster',
[ 'src/main.cpp' ],
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies)
executable('animator',
[ 'tools/animator.cpp' ],
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies)
executable('icongen',
[ 'tools/icongen.cpp' ],
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies)
executable('fragviewer',
[ 'tools/fragviewer.cpp' ],
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies)

View file

@ -0,0 +1 @@
magick montage -tile 3x1 -geometry +0+0 -background transparent .\assets\animations\torch_fixture_*.png assets/fixtures/torch_fixture.png

41
scripts/build_assets.ps1 Normal file
View file

@ -0,0 +1,41 @@
Param (
[string]$Colors=16,
[string]$Size="256x256"
)
function Build-Images {
param (
[string]$source,
[string]$pixel_count
)
$files = Get-ChildItem -Path "C:\Users\lcthw\Pictures\Games\Renders\Raycaster\$source"
$out_dir = ".\assets\" + $source.ToLower()
mkdir -force $out_dir
foreach($file in $files) {
$in_name = $file.Name
$out_file = "$out_dir\$in_name"
Write-Output "In file: $in_name"
Write-Output "Out file: $out_file"
Write-Output "Size: $Size"
.\scripts\pixelize.ps1 -InFile $file.FullName -OutFile $out_file -Colors $Colors -Pixel $pixel_count -Size $Size
}
}
# Build-Images -Source "Textures" -pixel_count 12
# Build-Images -Source "Sprites" -pixel_count 6
# Build-Images -Source "Items" -pixel_count 2
# Build-Images -Source "Animations" -pixel_count 24
# Build-Images -Source "Hands" -pixel_count 6
Build-Images -Source "Boss2" -pixel_count 4
# Build-Images -Source "Fixtures" -pixel_count 24
#magick montage -tile 3x1 -geometry +0+0 -background transparent .\assets\hands\female_hand_*.png .\assets\hands\female_hand.png
#magick montage -tile 3x1 -geometry +0+0 -background transparent .\assets\hands\male_hand_*.png .\assets\hands\male_hand.png
#cp -recurse -force C:\Users\lcthw\Pictures\Games\Renders\Raycaster\UI assets\ui

View file

@ -0,0 +1,13 @@
rm -recurse -force coverage/*
cp *.cpp,*.hpp,*.rl builddir
. .venv/Scripts/activate
rm -recurse -force coverage
cp scripts\gcovr_patched_coverage.py .venv\Lib\site-packages\gcovr\coverage.py
gcovr -o coverage/ --html --html-details --html-theme github.dark-blue --gcov-ignore-errors all --gcov-ignore-parse-errors negative_hits.warn_once_per_file -e builddir/subprojects -e builddir -e subprojects -e scratchpad -e tools -j 10 .
rm *.gcov.json.gz
start .\coverage\coverage_details.html

View file

@ -0,0 +1,7 @@
mv .\subprojects\packagecache .
rm -recurse -force .\subprojects\,.\builddir\
mkdir subprojects
mv .\packagecache .\subprojects\
mkdir builddir
cp wraps\*.wrap subprojects\
meson setup --default-library=static --prefer-static -Db_coverage=true builddir

11
scripts/coverage_reset.sh Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
mv -f ./subprojects/packagecache .
rm -rf subprojects builddir
mkdir subprojects
mv packagecache ./subprojects/
mkdir builddir
cp wraps/*.wrap subprojects/
# on OSX you can't do this with static
meson setup -Db_coverage=true builddir

File diff suppressed because it is too large Load diff

188
scripts/magick/pixelize Normal file
View file

@ -0,0 +1,188 @@
#!/bin/bash
#
# Developed by Fred Weinhaus 5/8/2008 .......... revised 4/25/2015
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: pixelize [-s size] [-m mode] infile outfile
# USAGE: pixelize [-h or -help]
#
# OPTIONS:
#
# -s size pixelization size; size>0; default=3
# -m mode mode of minimizing; 1=resize; 2=sample; default=1
#
###
#
# NAME: PIXELIZE
#
# PURPOSE: To create a pixelized or blocky effect in an image.
#
# DESCRIPTION: PIXELIZE creates a pixelized or blocky effect in an
# image where more pixelization (larger sizes) create larger blocky
# effects.
#
# OPTIONS:
#
# -s size ... SIZE is the pixelization (block) size. Values are greater
# than 0. The default is 3.
#
# -m mode ... MODE is the mode of minimizing. Choices are 1 for -resize
# and 2 for -sample. The default=1
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
size=3
mode=1
# set directory for temporary files
dir="." # suggestions are dir="." or dir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 6 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-h|-help) # help information
echo ""
usage2
exit 0
;;
-s) # get size
shift # to get the next parameter - scale
# test if parameter starts with minus sign
errorMsg="--- INVALID SIZE SPECIFICATION ---"
checkMinus "$1"
size=`expr "$1" : '\([0-9]*\)'`
[ "$size" = "" ] && errMsg "SIZE=$size MUST BE AN INTEGER"
sizetest=`echo "$size <= 0" | bc`
[ $sizetest -eq 1 ] && errMsg "--- SIZE=$size MUST BE A POSITIVE INTEGER ---"
;;
-m) # get mode
shift # to get the next parameter - mode
# test if parameter starts with minus sign
errorMsg="--- INVALID MODE SPECIFICATION ---"
checkMinus "$1"
mode=`expr "$1" : '\([0-9]*\)'`
[ "$mode" = "" ] && errMsg "MODE=$mode MUST BE AN INTEGER"
[ $mode -ne 1 -a $mode -ne 2 ] && errMsg "--- MODE=$mode MUST BE EITHER 1 OR 2 ---"
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"
fi
# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
# test if image an ordinary, readable and non-zero size
if [ -f $infile -a -r $infile -a -s $infile ]
then
: 'Do Nothing'
else
errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
fi
# get parameters
w=`convert $infile -format "%[fx:w]" info:`
h=`convert $infile -format "%[fx:h]" info:`
minify=`convert xc: -format "%[fx:100/$size]" info:`
# process image
if [ $mode -eq 1 ]; then
convert $infile -resize $minify% -scale ${w}x${h}! "$outfile"
elif [ $mode -eq 2 ]; then
convert $infile -sample $minify% -scale ${w}x${h}! "$outfile"
fi
exit 0

372
scripts/magick/position Normal file
View file

@ -0,0 +1,372 @@
#!/bin/bash
#
# Developed by Fred Weinhaus 2/24/2022 .......... revised 2/24/2022
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: position [-m method] [-d direction] [-o offset] [-l leftpt]
# [-r rightpt] [-b bcolor] [-f format] [-T trim] infile1 infile2 outfile
#
# USAGE: position [-h or -help]
#
# OPTIONS:
#
# -m method positioning method; choices are: offset or cpoints;
# default=offset
# -d direction positioning direction; choices are: horizontal or
# vertical; default=horizontal
# -o offset offset +X+Y values for left/ope edge of second image
# relative to right/bottom edge of first image. Used when
# method=offset; positive or negative offsets are allowed;
# default=+0+0
# -l leftpt left (first) image control x,y point; default=0,0
# -r rightpt right (second) image control x,y point; default=0,0
# -b bcolor background color to fill empty spaces
# -f format output color format; choices are: RG, GB, BR or RGB;
# default=RGB
# -T trim trim output to remove any background fill areas
#
###
#
# NAME: POSITION
#
# PURPOSE: To position one image relative to another image.
#
# DESCRIPTION: POSITION aligns or offsets one image relative to a another
# image. The second image is positioned relative to the first image either
# horizontally or vertically. Positioning can be done using X and Y offsets
# or by specifying one controll point for each image.
#
# OPTIONS:
#
# -m method ... positioning METHOD. The choices are: offset (o) or cpoints (c).
# The default=offset.
#
# -d direction ... positioning DIRECTION. The choices are: horizontal (h) or
# vertical (v). The default=horizontal.
#
# -o offset ... OFFSET +X+Y values for left/top edge of second image relative
# to right/bottom edge of first image. This is used when method=offset.
# Position X and Y offsets may be either positive or negative. The default=+0+0
#
# -l leftpt ... LEFT (first) image control x,y POINT. Values are integers>0.
# The default=0,0
#
# -r rightpt ... RIGHT (second) image control x,y POINT. Values are integers>0.
# The default=0,0
#
# -b bcolor ... BGCOLOR is the background color to fill empty spaces. Any
# Imagemagick color is allowed. The default=none (transparent)
#
# -f format ... output color FORMAT; The choices are: RG, GB, BR or RGB.
# RGB is the normal color image. RG, for example, is first image in Red and
# second image in Green and any overlay will show in yellow (mix of Red and
# Green). The default=RGB.
#
# -T trim ... TRIM output to remove any background fill areas.
# Choices are: yes (y) or no (n). The default=no. Background color must be
# unique in the image for the trim to work properly.
#
# LIMITATIONS: TRIM option only works for Imagemagick 7.0.9-0 or higher.
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
method="offset" # offset or cpoints
direction="horizontal" # horizontal or vertical
offset=+0+0 # offset
#offset=-90-40 # offset
leftpt="0,0" # left image single control point
rightpt="0,0" # right image single control point
#leftpt="287,49" # left image single control point
#rightpt="76,89" # right image single control point
bcolor=none # background color
format="RGB" # RG or GB or BR or RGB output color format
trim="no" # trim output; yes or no
# set directory for temporary files
tmpdir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 19 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-h|-help) # help information
echo ""
usage2
exit 0
;;
-m) # method
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID METHOD SPECIFICATION ---"
checkMinus "$1"
method=`echo "$1" | tr "[:upper:]" "[:lower:]"`
case "$method" in
offset|o) method="offset" ;;
cpoints|c) method="cpoints" ;;
*) errMsg "--- METHOD=$method IS AN INVALID VALUE ---"
esac
;;
-d) # direction
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID DIRECTION SPECIFICATION ---"
checkMinus "$1"
direction=`echo "$1" | tr "[:upper:]" "[:lower:]"`
case "$direction" in
horizontal|h) direction="horizontal" ;;
vertical|v) direction="vertical" ;;
*) errMsg "--- DIRECTION=$direction IS AN INVALID VALUE ---"
esac
;;
-o) # offset
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID OFFSET SPECIFICATION ---"
#checkMinus "$1"
offset=`expr "$1" : '\([-+][0-9]*[-+][0-9]*\)'`
[ "$offset" = "" ] && errMsg "--- OFFSET=$offset IS INVALID ---"
;;
-l) # leftpt
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID LEFTPT SPECIFICATION ---"
checkMinus "$1"
leftpt=`expr "$1" : '\([0-9]*,[0-9]*\)'`
[ "$leftpt" = "" ] && errMsg "--- LEFTPT=$leftpt IS INVALID ---"
;;
-r) # rightpt
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID RIGHTPT SPECIFICATION ---"
checkMinus "$1"
rightpt=`expr "$1" : '\([0-9]*,[0-9]*\)'`
[ "$rightpt" = "" ] && errMsg "--- RIGHTPT=$rightpt IS INVALID ---"
;;
-b) # bcolor
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID BCOLOR SPECIFICATION ---"
checkMinus "$1"
bcolor="$1"
;;
-f) # format
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID FORMAT SPECIFICATION ---"
checkMinus "$1"
format=`echo "$1" | tr "[:lower:]" "[:upper:]"`
case "$format" in
RG) ;;
GB) ;;
BR) ;;
RGB) ;;
*) errMsg "--- FORMAT=$format IS AN INVALID VALUE ---"
esac
;;
-T) # trim
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID TRIM SPECIFICATION ---"
checkMinus "$1"
trim=`echo "$1" | tr "[:upper:]" "[:lower:]"`
case "$trim" in
yes) ;;
no) ;;
*) errMsg "--- TRIM=$trim IS AN INVALID VALUE ---"
esac
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infiles and outfile
infile1="$1"
infile2="$2"
outfile="$3"
fi
# test that infile1 provided
[ "$infile1" = "" ] && errMsg "--- NO INPUT FILE 1 SPECIFIED ---"
# test that infile2 provided
[ "$infile2" = "" ] && errMsg "--- NO INPUT FILE 2 SPECIFIED ---"
# test that outfile provided
[ "$outfile" = "" ] && errMsg "--- NO OUTPUT FILE SPECIFIED ---"
dir="$tmpdir/POSITION.$$"
mkdir "$dir" || echo "--- FAILED TO CREATE TEMPORARY FILE DIRECTORY ---"
trap "rm -rf $dir; exit 0" 0
trap "rm -rf $dir; exit 1" 1 2 3 15
# read input images
# test if infile exists, is readable and is not zero size
convert -quiet "$infile1" +repage $dir/tmpI1.mpc ||
echo "--- FILE $infile1 DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
convert -quiet "$infile2" +repage $dir/tmpI2.mpc ||
echo "--- FILE $infile2 DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
# get image1 dimensions
ww=`convert $dir/tmpI1.mpc -format "%w" info:`
hh=`convert $dir/tmpI1.mpc -format "%h" info:`
#echo "ww=$ww; hh=$hh;"
# get page values for second image
if [ "$method" = "cpoints" ]; then
lx=`echo "$leftpt" | cut -d, -f1`
ly=`echo "$leftpt" | cut -d, -f2`
rx=`echo "$rightpt" | cut -d, -f1`
ry=`echo "$rightpt" | cut -d, -f2`
pagex=$((lx-rx))
pagey=$((ly-ry))
else # offsets
xoff=`echo $offset | sed -n 's/^\([+-].*\)[+-].*$/\1/p'`
yoff=`echo $offset | sed -n 's/^[+-].*\([+-].*\)$/\1/p'`
if [ "$direction" = "horizontal" ]; then
pagex=$((ww+xoff))
pagey=$((yoff))
else
# vertical
pagex=$((xoff))
pagey=$((hh+yoff))
fi
fi
#echo "ww=$ww; hh=$hh; xoff=$xoff; yoff=$yoff; lx=$lx; ly=$ly; rx=$rx; ry=$ry; pagex=$pagex; pagey=$pagey;"
# set up for trim
[ "$trim" = "yes" ] && trimming="-background $bcolor -define trim:percent-background=0% -trim +repage"
# align the two images
if [ "$format" = "RG" ]; then
convert \
\( $dir/tmpI1.mpc -colorspace gray -set page +0+0 -write mpr:img1 +delete \) \
\( $dir/tmpI2.mpc -colorspace gray -set page +${pagex}+${pagey} -write mpr:img2 +delete \) \
\( \( mpr:img2 -background black -colorize 100 \) \( mpr:img1 +level-colors "black,red" \) \
-background "$bcolor" -layers merge +repage \) \
\( \( mpr:img1 -background black -colorize 100 \) \( mpr:img2 +level-colors "black,green1" \) \
-background "$bcolor" -layers merge +repage \) \
-compose over -compose blend -composite $trimming \
"$outfile"
elif [ "$format" = "GB" ]; then
convert \
\( $dir/tmpI1.mpc -colorspace gray -set page +0+0 -write mpr:img1 +delete \) \
\( $dir/tmpI2.mpc -colorspace gray -set page +${pagex}+${pagey} -write mpr:img2 +delete \) \
\( \( mpr:img2 -background black -colorize 100 \) \( mpr:img1 +level-colors "black,green1" \) \
-background "$bcolor" -layers merge +repage \) \
\( \( mpr:img1 -background black -colorize 100 \) \( mpr:img2 +level-colors "black,blue" \) \
-background "$bcolor" -layers merge +repage \) \
-compose over -compose blend -composite $trimming \
"$outfile"
elif [ "$format" = "BR" ]; then
convert \
\( $dir/tmpI1.mpc -colorspace gray -set page +0+0 -write mpr:img1 +delete \) \
\( $dir/tmpI2.mpc -colorspace gray -set page +${pagex}+${pagey} -write mpr:img2 +delete \) \
\( \( mpr:img2 -background black -colorize 100 \) \( mpr:img1 +level-colors "black,blue" \) \
-background "$bcolor" -layers merge +repage \) \
\( \( mpr:img1 -background black -colorize 100 \) \( mpr:img2 +level-colors "black,red" \) \
-background "$bcolor" -layers merge +repage \) \
-compose over -compose blend -composite $trimming \
"$outfile"
else # RGB
convert \
\( $dir/tmpI1.mpc -set page +0+0 \) \
\( $dir/tmpI2.mpc -set page +${pagex}+${pagey} \) \
-background "$bcolor" -layers merge +repage $trimming \
"$outfile"
fi
exit 0

268
scripts/magick/splitcrop Normal file
View file

@ -0,0 +1,268 @@
#!/bin/bash
#
# Developed by Fred Weinhaus revised 6/9/2012 .......... revised 4/25/2015
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: splitcrop [-x xcoord] [-y ycoord] [-L] infile [outfile]
# USAGE: splitcrop [-h or -help]
#
# OPTIONS:
#
# -x xcoord x coordinate for split; 0<integer<width; default=center
# -y xcoord y coordinate for split; 0<integer<height; default=center
# -L list crop dimensions and offsets to the terminal
#
# Note, the output images will be named automatically from the outfile name and
# suffix. Two or four of the following: _left, _right, _top, _bottom,
# _topleft, _topright, _bottomleft, _bottomright will be appended before the
# suffix. If no outfile is provided, then the infile name and suffix will be
# used for the output.
#
###
#
# NAME: SPLITCROP
#
# PURPOSE: To crop an image into two or four sections according to the given
# x,y coordinates.
#
# DESCRIPTION: SPLITCROP crops an image into two or four sections according to
# the given x,y coordinates. One or both of the x,y coordinates may be
# specified. If one coordinate is specified, then the image will be split into
# two parts. If two coordinate are specified, then the image will be split both
# ways into four parts. Note that these are coordinates and not sizes. The top,
# left or topleft section will include the coordinate specified. The size of
# the split will be the coordinate plus 1. If the image dimension is odd,
# then the top, left or topleft will contain the extra pixel(s).
#
# OPTIONS:
#
# -x xcoord ... XCOORD is the x coordinate for the split. Values are
# 0<integers<width. The default=center of image
#
# -y ycoord ... YCOORD is the y coordinate for the split. Values are
# 0<integers<height. The default=center of image
#
# -L ... LIST crop dimensions and offsets to the terminal
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
xcoord="" # x coordinate for split
ycoord="" # y coordinate for split
list="off"
# set directory for temporary files
dir="." # suggestions are dir="." or dir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME="splitcrop" # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 6 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-h|-help) # help information
echo ""
usage2
exit 0
;;
-x) # get xcoord
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID XCOORD SPECIFICATION ---"
checkMinus "$1"
xcoord=`expr "$1" : '\([0-9]*\)'`
[ "$xcoord" = "" ] && errMsg "--- XCOORD=$xcoord MUST BE A NON-NEGATIVE INTEGER ---"
;;
-y) # get ycoord
shift # to get the neyt parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID YCOORD SPECIFICATION ---"
checkMinus "$1"
ycoord=`expr "$1" : '\([0-9]*\)'`
[ "$ycoord" = "" ] && errMsg "--- YCOORD=$ycoord MUST BE A NON-NEGATIVE INTEGER ---"
;;
-L) # get list
list="on"
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"
fi
# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
# test that outfile provided
if [ "$outfile" = "" ]; then
# separate infile to outname and suffix
outname=`echo "$infile" | sed -n 's/^\(.*\)[\.].*$/\1/p'`
suffix=`echo "$infile" | sed -n 's/^.*[\.]\(.*\)$/\1/p'`
else
# separate outfile to outname and suffix
outname=`echo "$outfile" | sed -n 's/^\(.*\)[\.].*$/\1/p'`
suffix=`echo "$outfile" | sed -n 's/^.*[\.]\(.*\)$/\1/p'`
fi
# setup temporary images
tmpA1="$dir/split_A_$$.mpc"
tmpA2="$dir/split_A_$$.cache"
trap "rm -f $tmpA1 $tmpA2;" 0
trap "rm -f $tmpA1 $tmpA2; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpA2; exit 1" ERR
# read the input image into the temporary cached image and test if valid
convert -quiet "$infile" +repage "$tmpA1" ||
errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO size ---"
# get input dimensions
ww=`convert $tmpA1 -format "%w" info:`
hh=`convert $tmpA1 -format "%h" info:`
#echo "ww=$ww; hh=$hh"
x=$xcoord
y=$ycoord
# trap for no coordinates specified
if [ "$x" = "" -a "$y" = "" ]; then
x=`convert xc: -format "%[fx:round($ww/2)-1]" info:`
y=`convert xc: -format "%[fx:round($hh/2)-1]" info:`
fi
# trap for coordinates at boundary or outside image
if [ "$x" != "" ]; then
( [ $x -eq 0 ] || [ $x -ge $ww ] ) && errMsg "--- INVALID X COORDINATE SPECIFIED ---"
fi
if [ "$y" != "" ]; then
( [ $y -eq 0 ] || [ $y -ge $ww ] ) && errMsg "--- INVALID Y COORDINATE SPECIFIED ---"
fi
# options depending upon whether x or y or (x and y) specified
if [ "$x" != "" -a "$y" != "" ]; then
#split into four parts
tlsize=`convert xc: -format "%[fx:$x+1]x%[fx:$y+1]+0+0" info:`
trsize=`convert xc: -format "%[fx:$ww-$x-1]x%[fx:$y+1]+%[fx:$x+1]+0" info:`
blsize=`convert xc: -format "%[fx:$x+1]x%[fx:$hh-$y-1]+0+%[fx:$y+1]" info:`
brsize=`convert xc: -format "%[fx:$ww-$x-1]x%[fx:$hh-$y-1]+%[fx:$x+1]+%[fx:$y+1]" info:`
if [ "$list" = "on" ]; then
echo "tlsize=$tlsize"
echo "trsize=$trsize"
echo "blsize=$blsize"
echo "brsize=$brsize"
fi
convert $tmpA1 -crop $tlsize +repage "${outname}_topleft.$suffix"
convert $tmpA1 -crop $trsize +repage "${outname}_topright.$suffix"
convert $tmpA1 -crop $blsize +repage "${outname}_bottomleft.$suffix"
convert $tmpA1 -crop $brsize +repage "${outname}_bottomright.$suffix"
elif [ "$x" != "" ]; then
#split into two parts horizontally
lsize=`convert xc: -format "%[fx:$x+1]x${hh}+0+0" info:`
rsize=`convert xc: -format "%[fx:$ww-$x-1]x${hh}+%[fx:$x+1]+0" info:`
if [ "$list" = "on" ]; then
echo "lsize=$lsize"
echo "rsize=$rsize"
fi
convert $tmpA1 -crop $lsize +repage "${outname}_left.$suffix"
convert $tmpA1 -crop $rsize +repage "${outname}_right.$suffix"
elif [ "$y" != "" ]; then
#split into two parts vertically
tsize=`convert xc: -format "${ww}x%[fx:$y+1]+0+0" info:`
bsize=`convert xc: -format "${ww}x%[fx:$hh-$y-1]+0+%[fx:$y+1]" info:`
if [ "$list" = "on" ]; then
echo "tsize=$tsize"
echo "bsize=$bsize"
fi
convert $tmpA1 -crop $tsize +repage "${outname}_top.$suffix"
convert $tmpA1 -crop $bsize +repage "${outname}_bottom.$suffix"
fi
exit 0

514
scripts/magick/stainedglass Normal file
View file

@ -0,0 +1,514 @@
#!/bin/bash
#
# Developed by Fred Weinhaus 6/12/2010 .......... 11/12/2017
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: stainedglass [-k kind] [-s size] [-o offset] [-n ncolors] [-b bright]
# [-e ecolor] [-t thick] [-r rseed] [-a] infile outfile
# USAGE: stainedglass [-h or -help]
#
# OPTIONS:
#
# -k kind kind of stainedglass cell shape; choices are: square
# (or s), hexagon (or h), random (or r); default=random
# -s size size of cell; integer>0; default=16
# -o offset random offset amount; integer>=0; default=6;
# only applies to kind=random
# -n ncolors number of desired reduced colors for the output;
# integer>1; default is no color reduction
# -b bright brightness value in percent for output image;
# integer>=0; default=100
# -e ecolor color for edge or border around each cell; any valid
# IM color; default=black
# -t thick thickness for edge or border around each cell;
# integer>=0; default=1; zero means no edge or border
# -r rseed random number seed value; integer>=0; if seed provided,
# then image will reproduce; default is no seed, so that
# each image will be randomly different; only applies
# to kind=random
# -a use average color of cell rather than color at center
# of cell; default is center color
#
###
#
# NAME: STAINEDGLASS
#
# PURPOSE: Applies a stained glass cell effect to an image.
#
# DESCRIPTION: STAINEDGLASS applies a stained glass cell effect to an image. The
# choices of cell shapes are hexagon, square and randomized square. The cell
# size and border around the cell can be specified.
#
#
# OPTIONS:
#
# -k kind ... KIND of stainedglass cell shape; choices are: square (or s),
# hexagon (or h), random (or r). The latter is a square with each corner
# randomly offset. The default=random.
#
# -s size ... SIZE of stained glass cells. Values are integers>=0. The
# default=16.
#
# -o offset ... OFFSET is the random offset amount for the case of kind=random.
# Values are integers>=0. The default=6.
#
# -n ncolors ... NCOLORS is the number of desired reduced colors in the output.
# Values are integers>1. The default is no color reduction. Larger number of
# colors takes more time to color reduce.
#
# -b bright ... BRIGHTNESS value in percent for the output image. Values are
# integers>=0. The default=100 means no change in brightness.
#
# -e ecolor ... ECOLOR is the color for the edge or border around each cell.
# Any valid IM color is allowed. The default=black.
#
# -t thick ... THICK is the thickness for the edge or border around each cell.
# Values are integers>=0. The default=1. A value of zero means no edge or
# border will be included.
#
# -r rseed ... RSEED is the random number seed value for kind=random. Values
# are integers>=0. If a seed is provided, then the resulting image will be
# reproducable. The default is no seed. In that case, each resulting image
# will be randomly different.
#
# -a ... use AVERAGE color of cell rather than color at center of shell;
# default is center color. The average value will be accurate only for odd
# square shapes with IM 6.5.9.0 or higher. All others cases will use only an
# approximate average.
#
# Thanks to Anthony Thyssen for critiqing the original version and for
# several useful suggestions for improvement.
#
# NOTE: This script will be slow prior to IM 6.8.3.10 due to the need to
# extract color values for each cell center point across the input image.
# A progress meter is therefore provided to the terminal. A speed-up is
# available via a -process function, getColors. To obtain getColors,
# contact me. It requires IM 6.6.2-10 or higher.
#
# IMPORTANT: This script will fail due to an unintended restriction in the
# txt: format starting with IM 6.9.9.1 and IM 7.0.6.2. It has been fixed at
# IM 6.9.9.23 and IM 7.0.7.11.
#
# REQUIREMENTS: Does not produce a proper set of edges/borders around each
# cell under Q8, due to insufficient graylevel resolution (0 and 255)
# to handle more that 255 cells.
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
kind="random" # random, square, hexagon
size=16 # cell size
offset=6 # pixel amount to randomly add or subtract to square corners
ncolors="" # number of output colors
bright=100 # brightness adjust
ecolor="black" # edge color
thick=1 # edge thickness
rseed="" # seed for random
average="no" # preprocess for cell average
# set directory for temporary files
dir="." # suggestions are dir="." or dir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 19 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-help) # help information
echo ""
usage2
exit 0
;;
-k) # get kind
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID KIND SPECIFICATION ---"
checkMinus "$1"
kind=`echo "$1" | tr '[A-Z]' '[a-z]'`
case "$kind" in
hexagon|h) kind="hexagon" ;;
square|s) kind="square" ;;
random|r) kind="random" ;;
*) errMsg "--- KIND=$kind IS AN INVALID VALUE ---" ;;
esac
;;
-s) # get size
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID SIZE SPECIFICATION ---"
checkMinus "$1"
size=`expr "$1" : '\([0-9]*\)'`
[ "$size" = "" ] && errMsg "--- SIZE=$size MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
test1=`echo "$size < 1" | bc`
[ $test1 -eq 1 ] && errMsg "--- SIZE=$size MUST BE A POSITIVE INTEGER ---"
;;
-o) # get offset
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID OFFSET SPECIFICATION ---"
checkMinus "$1"
offset=`expr "$1" : '\([0-9]*\)'`
[ "$offset" = "" ] && errMsg "--- OFFSET=$offset MUST BE A NON-NEGATIVE INTEGER ---"
;;
-n) # get ncolors
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID NCOLORS SPECIFICATION ---"
checkMinus "$1"
ncolors=`expr "$1" : '\([0-9]*\)'`
[ "$ncolors" = "" ] && errMsg "--- NCOLORS=$ncolors MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
test1=`echo "$ncolors < 2" | bc`
[ $test1 -eq 1 ] && errMsg "--- NCOLORS=$ncolors MUST BE AN GREATER THAN 1 ---"
;;
-b) # get bright
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID BRIGHT SPECIFICATION ---"
checkMinus "$1"
bright=`expr "$1" : '\([0-9]*\)'`
[ "$bright" = "" ] && errMsg "--- BRIGHT=$bright MUST BE A NON-NEGATIVE INTEGER ---"
;;
-e) # get ecolor
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID ECOLOR SPECIFICATION ---"
checkMinus "$1"
ecolor="$1"
;;
-t) # get thick
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID THICK SPECIFICATION ---"
checkMinus "$1"
thick=`expr "$1" : '\([0-9]*\)'`
[ "$thick" = "" ] && errMsg "--- THICK=$thick MUST BE A NON-NEGATIVE INTEGER ---"
;;
-r) # get rseed
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID RSEED SPECIFICATION ---"
checkMinus "$1"
rseed=`expr "$1" : '\([0-9]*\)'`
[ "$rseed" = "" ] && errMsg "--- RSEED=$rseed MUST BE A NON-NEGATIVE INTEGER ---"
;;
-a) # get average
average="yes"
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"
fi
# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
# setup temporary images and auto delete upon exit
tmpA1="$dir/stainedglass_1_$$.mpc"
tmpB1="$dir/stainedglass_1_$$.cache"
tmpA2="$dir/stainedglass_2_$$.mpc"
tmpB2="$dir/stainedglass_2_$$.cache"
tmpA3="$dir/stainedglass_3_$$.mpc"
tmpB3="$dir/stainedglass_3_$$.cache"
tmpC="$dir/stainedglass_C_$$.txt"
tmpG="$dir/stainedglass_G_$$.txt"
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG;" 0
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG; exit 1" 1 2 3 15
# does not seem to produce output with the following????
#trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG; exit 1" ERR
# set up color reduction
if [ "$ncolors" != "" ]; then
reduce="-monitor +dither -colors $ncolors +monitor"
echo ""
echo "Reducing Colors:"
else
reduce=""
fi
im_version=`convert -list configure | \
sed '/^LIB_VERSION_NUMBER */!d; s//,/; s/,/,0/g; s/,0*\([0-9][0-9]\)/\1/g' | head -n 1`
# colorspace swapped at IM 6.7.5.5, but not properly fixed until 6.7.6.6
# before swap verbose info reported colorspace=RGB after colorspace=sRGB
if [ "$im_version" -ge "06070606" ]; then
cspace1="sRGB"
cspace2="sRGBA"
else
cspace1="RGB"
cspace2="RGBA"
fi
# no need for setcspace for grayscale or channels after 6.8.5.4
if [ "$im_version" -gt "06080504" ]; then
cspace1=""
cspace2=""
fi
# get colorspace
colorspace=`convert $infile -ping -format "%[colorspace]" info:`
# set up for modulate
# note there seems to be a change in -modulate (for HSL) between IM 6.8.4.6 and 6.8.4.7 that is noticeable in the output
if [ "$bright" != "100" -a "$colorspace" = "CMYK" ]; then
modulation="-colorspace $cspace1 -modulate ${bright},100,100 -colorspace cmyk"
elif [ "$bright" != "100" -a "$colorspace" = "CMYKA" ]; then
modulation="-colorspace $cspace2 -modulate ${bright},100,100 -colorspace cmyka"
elif [ "$bright" != "100" ]; then
modulation="-modulate ${bright},100,100"
else
modulation=""
fi
# read the input image and filter image into the temp files and test validity.
convert -quiet "$infile" $reduce $modulation +repage "$tmpA1" ||
errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
# preprocess for average color
if [ "$average" = "yes" -a "$im_version" -ge "06050900" -a "$kind" = "square" ]; then
dim=`convert xc: -format "%[fx:round(($size-1)/2)]" info:`
convert $tmpA1 -define convolve:scale=! -morphology convolve square:$dim $tmpA1
elif [ "$average" = "yes" -a "$im_version" -ge "06050900" -a "$kind" != "square" ]; then
dim=`convert xc: -format "%[fx:round(($size-1)/2)+0.5]" info:`
convert $tmpA1 -define convolve:scale=! -morphology convolve disk:$dim $tmpA1
elif [ "$average" = "yes" ]; then
dim=`convert xc: -format "%[fx:round(($size-1)/2)]" info:`
convert $tmpA1 -blur ${dim}x65000 $tmpA1
fi
# test if -process module getcolors exists
if [ "$im_version" -ge "06050210" ]; then
process_test=`convert -list module | grep "getColors"`
fi
#echo "process_test=$process_test;"
ww=`convert $tmpA1 -ping -format "%w" info:`
hh=`convert $tmpA1 -ping -format "%h" info:`
ww1=$(($ww-1))
hh1=$(($hh-1))
ww2=`convert xc: -format "%[fx:$ww1+round($size/2)]" info:`
hh2=`convert xc: -format "%[fx:$hh1+round($size/2)]" info:`
#echo "ww=$ww; hh=$hh; ww1=$ww1; hh1=$hh1; ww2=$ww2; hh2=$hh2;"
# get qrange
qrange=`convert xc: -format "%[fx:quantumrange]" info:`
# init colors file
echo "# ImageMagick pixel enumeration: $ww,$hh,255,rgb" > $tmpC
# init increment grays file
touch $tmpG
if [ "$kind" = "random" ]; then
# need to add 1 to offset as awk rand is exclusive between 0 and 1
offset=$(($offset+1))
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v offset="$offset" -v rseed="$rseed" '
BEGIN { if (rseed=="") {srand();} else {srand(rseed);} y=0; while ( y < hh2 )
{ x=0; while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;}
rx=rand(); if (rx<0.5) {signx=-1;} else {signx=1;}
offx=int(signx*rand()*offset);
xx=x+offx; if (xx<0) {xx=0}; if (xx>ww1) {xx=ww1};
ry=rand(); if (ry<0.5) {signy=-1;} else {signy=1;}
offy=int(signy*rand()*offset);
yy=y+offy; if (yy<0) {yy=0}; if (yy>hh1) {yy=hh1};
print xx","yy": (255,255,255)"; x=x+size; }
y=y+size; } }' >> $tmpC
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" -v offset="$offset" -v rseed="$rseed" '
BEGIN { if (rseed=="") {srand();} else {srand(rseed);} k=0; y=0; while ( y < hh2 )
{ x=0; while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;}
rx=rand(); if (rx<0.5) {signx=-1;} else {signx=1;}
offx=int(signx*rand()*offset);
xx=x+offx; if (xx<0) {xx=0}; if (xx>ww1) {xx=ww1};
ry=rand(); if (ry<0.5) {signy=-1;} else {signy=1;}
offy=int(signy*rand()*offset);
yy=y+offy; if (yy<0) {yy=0}; if (yy>hh1) {yy=hh1};
g=(k % 256);
print xx,yy" gray("g")"; k++; x=x+size; }
y=y+size; } }' >> $tmpG
elif [ "$kind" = "hexagon" ]; then
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" '
BEGIN { j=0; y=0; while ( y < hh2 )
{ if (j%2==0) {x=int((size+0.5)/2);} else {x=0;} while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} print x","y": (255,255,255)"; x=x+size; }
j++; y=y+size; } }' >> $tmpC
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" '
BEGIN { j=0; k=0; y=0; while ( y < hh2 )
{ if (j%2==0) {x=int((size+0.5)/2);} else {x=0;} while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} g=(k % 256); print x,y" gray("g")"; k++; x=x+size; }
j++; y=y+size; } }' >> $tmpG
elif [ "$kind" = "square" ]; then
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" '
BEGIN { y=0; while ( y < hh2 )
{ x=0; while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} print x","y": (255,255,255)"; x=x+size; }
y=y+size; } }' >> $tmpC
awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" '
BEGIN { k=0; y=0; while ( y < hh2 )
{ x=0; while (x < ww2 )
{ if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} g=(k % 256); print x,y" gray("g")"; k++; x=x+size; }
y=y+size; } }' >> $tmpG
fi
if [ "$thick" = "0" ]; then
if [ "$im_version" -ge "06080310" ]; then
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite \
sparse-color:- |\
convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' \
"$outfile"
elif [ "$im_version" -lt "06060210" -o "$process_test" != "getColors" ]; then
echo ""
echo "Progress:"
echo ""
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite -monitor \
txt:- |\
sed '1d; / 0) /d; s/:.* /,/;' |\
convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' \
+monitor "$outfile"
echo ""
else
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite $tmpA1
convert $tmpA1 -alpha on -process "getColors" null: > $tmpC
convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpC" \
"$outfile"
fi
else
if [ "$im_version" -ge "06080310" ]; then
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite \
sparse-color:- |\
convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' $tmpA2
convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \
-auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3
convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \
-compose over -background $ecolor -flatten \
"$outfile"
elif [ "$im_version" -lt "06060210" -o "$process_test" != "getColors" ]; then
echo ""
echo "Progress:"
echo ""
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite -monitor \
txt:- |\
sed '1d; / 0) /d; s/:.* /,/;' |\
convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' +monitor $tmpA2
convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \
-auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3
convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \
-compose over -background $ecolor -flatten \
"$outfile"
echo ""
else
convert $tmpA1 \( -background black $tmpC \) \
-alpha off -compose copy_opacity -composite $tmpA1
convert $tmpA1 -alpha on -process "getColors" null: > $tmpC
convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpC" $tmpA2
convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \
-auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3
convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \
-compose over -background $ecolor -flatten \
"$outfile"
fi
fi
exit 0

View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -ex
./splitcrop -x $1 -y $1 $2 corners.png
./position -d horizontal corners_bottomright.png corners_bottomleft.png bottom.png
./position -d horizontal corners_topright.png corners_topleft.png top.png
./position -d vertical bottom.png top.png result-grid.png

347
scripts/magick/tileimage Normal file
View file

@ -0,0 +1,347 @@
#!/bin/bash
#
# Developed by Fred Weinhaus 8/10/2015 .......... revised 8/10/2015
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: tileimage [-a arrangement] [-o orientation ] [-r repeats] [-w width ]
# [-h height] [-b bgcolor] infile outfile
#
# USAGE: tileimage [-help]
#
# OPTIONS:
#
# -a arrangement tiling arrangement; choices are: repeat, halfdrop,
# brick, verticalmirror, horizontalmirror or rotate;
# default=repeat
# -o orientation orientation mode for image repeats; choices are: 0,
# 90, 180, 270, flip, flop, transpose, transverse;
# default=0
# -r repeats number of horizontal repeats; integer>0; default=4
# -w width width of canvas in pixels; integer>0; default=512
# -h height height of canvas in pixels; integer>0; default=512
# -b bgcolor background color for canvas, if image is transparent;
# any valid IM color; default="white"
#
###
#
# NAME: TILEIMAGE
#
# PURPOSE: To tile an image to a given size with various tile arrangements.
#
# DESCRIPTION: TILEIMAGE tiles an image to a given size. Several arrangements
# are possible as well as several orientations. The user can also specify the
# number of repeats across the width of the image. This script is typically
# used to tile objects on a constant color background.
#
# OPTIONS:
#
# -a arrangement ... tiling ARRANGEMENT. The choices are: repeat (r),
# halfdrop (h), brick (b), verticalmirror (vm), horizontalmirror (hm), or .
# rotate (ro). The default=repeat.
#
# -o orientation ... ORIENTATION mode for the image repeats. The choices are:
# 0 (degree rotation), 90 (degree rotation), 180 (degree rotation),
# 270 (degree rotation), flip, flop, transpose, transverse. The default=0.
#
# -r repeats ... REPEATS is the number of horizontal repeats across the image.
# Values are integers>0. The default=4.
#
# -w width ... WIDTH of output image in pixels. Values are integers>0. The
# default=512.
#
# -h height ... HEIGHT of output image in pixels. Values are integers>0. The
# default=512.
#
# -b bgcolor ... BGCOLOR is the desired background color for the output image,
# if the input image is transparent. Any valid IM color. The default="white".
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
arrangement="repeat"
orientation="0"
repeats=4
width=512
height=512
bgcolor="white"
# set directory for temporary files
dir="." # suggestions are dir="." or dir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -n '/^###/q; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage1
exit 0
elif [ $# -gt 14 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-help) # help information
echo ""
usage1
exit 0
;;
-a) # arrangement
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID ARRANGEMENT SPECIFICATION ---"
checkMinus "$1"
# test gravity values
arrangement="$1"
arrangement=`echo "$1" | tr '[A-Z]' '[a-z]'`
case "$arrangement" in
repeat|r) arrangement="repeat" ;;
halfdrop|h) arrangement="halfdrop" ;;
brick|b) arrangement="brick" ;;
verticalmirror|vm) arrangement="verticalmirror" ;;
horizontalmirror|hm) arrangement="horizontalmirror" ;;
rotate|ro) arrangement="rotate" ;;
*) errMsg "--- ARRANGEMENT=$arrangement IS AN INVALID OPTION ---" ;;
esac
;;
-o) # orientation
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID ORIENTATION SPECIFICATION ---"
checkMinus "$1"
# test gravity values
orientation="$1"
orientation=`echo "$1" | tr '[A-Z]' '[a-z]'`
case "$orientation" in
0) orientation="0" ;;
90) orientation="90" ;;
180) orientation="180" ;;
270) orientation="270" ;;
flip) orientation="flip" ;;
flop) orientation="flop" ;;
transpose) orientation="transpose" ;;
transverse) orientation="transverse" ;;
*) errMsg "--- ORIENTATION=$orientation IS AN INVALID OPTION ---" ;;
esac
;;
-w) # get width
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID WIDTH SPECIFICATION ---"
checkMinus "$1"
width=`expr "$1" : '\([.0-9]*\)'`
[ "$width" = "" ] && errMsg "--- WIDTH=$width MUST BE A NON-NEGATIVE FLOAT ---"
test=`echo "$width <= 0" | bc`
[ $test -eq 1 ] && errMsg "--- WIDTH=$width MUST BE A POSITIVE FLOAT ---"
;;
-h) # get height
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID HEIGHT SPECIFICATION ---"
checkMinus "$1"
height=`expr "$1" : '\([.0-9]*\)'`
[ "$height" = "" ] && errMsg "--- HEIGHT=$height MUST BE A NON-NEGATIVE FLOAT ---"
test=`echo "$height <= 0" | bc`
[ $test -eq 1 ] && errMsg "--- HEIGHT=$height MUST BE A POSITIVE FLOAT ---"
;;
-r) # get repeats
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID REPEATS SPECIFICATION ---"
checkMinus "$1"
repeats=`expr "$1" : '\([0-9]*\)'`
[ "$repeats" = "" ] && errMsg "--- REPEATS=$repeats MUST BE A NON-NEGATIVE INTEGER ---"
test=`echo "$repeats == 0" | bc`
[ $test -eq 1 ] && errMsg "--- REPEATS=$repeats MUST BE A POSITIVE INTEGER ---"
;;
-b) # get bgcolor
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID BGCOLOR SPECIFICATION ---"
checkMinus "$1"
bgcolor="$1"
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"
fi
# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
# setup temporary images
tmpA1="$dir/tileimage_1_$$.mpc"
tmpB1="$dir/tileimage_1_$$.cache"
trap "rm -f $tmpA1 $tmpB1;" 0
trap "rm -f $tmpA1 $tmpB1; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpB1; exit 1" ERR
# read the input image into the temporary cached image and test if valid
# read the input image into the temporary cached image and test if valid
convert -quiet "$infile" +repage "$tmpA1" ||
errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO size ---"
# get infile dimensions
ww=`identify -ping -format "%w" $tmpA1`
hh=`identify -ping -format "%h" $tmpA1`
# get scaling factor for infile in percent
factor=`convert xc: -format "%[fx:100*$width/($repeats*$ww)]" info:`
# scale the infile
convert $tmpA1 -resize $factor% $tmpA1
# compute resized dimensions
ww=`identify -ping -format "%w" $tmpA1`
hh=`identify -ping -format "%h" $tmpA1`
# get half sizes for offsets
ww2=`convert xc: -format "%[fx:round($ww/2)]" info:`
hh2=`convert xc: -format "%[fx:round($hh/2)]" info:`
# set up orientation
if [ "$orientation" = "0" ]; then
orienting=""
elif [ "$orientation" = "90" ]; then
orienting="-rotate 90"
elif [ "$orientation" = "180" ]; then
orienting="-rotate 180"
elif [ "$orientation" = "270" ]; then
orienting="-rotate 270"
elif [ "$orientation" = "flip" ]; then
orienting="-flip"
elif [ "$orientation" = "flop" ]; then
orienting="-flop"
elif [ "$orientation" = "transpose" ]; then
orienting="-transpose"
elif [ "$orientation" = "transverse" ]; then
orienting="-transverse"
fi
# process image
if [ "$arrangement" = "repeat" ]; then
#echo "repeat; orient=$orienting; width=$width; height=$height; bgcolor=$bgcolor;"
convert $tmpA1 $orienting -write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background $bgcolor -flatten "$outfile"
elif [ "$arrangement" = "halfdrop" ]; then
convert $tmpA1 \
\( -clone 0 $orienting -roll +0+${hh2} \) +append \
-write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background "$bgcolor" -flatten "$outfile"
elif [ "$arrangement" = "brick" ]; then
convert $tmpA1 \
\( -clone 0 $orienting -roll +${ww2}+0 \) -append \
-write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background "$bgcolor" -flatten "$outfile"
elif [ "$arrangement" = "verticalmirror" ]; then
convert $tmpA1 \
\( -clone 0 $orienting \) +append \
\( -clone 0 -flip \) -append \
-write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background "$bgcolor" -flatten "$outfile"
elif [ "$arrangement" = "horizontalmirror" ]; then
convert $tmpA1 \
\( -clone 0 $orienting \) -append \
\( -clone 0 -flop \) +append \
-write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background "$bgcolor" -flatten "$outfile"
elif [ "$arrangement" = "rotate" ]; then
convert $tmpA1 $orienting \
\( -clone 0 -rotate 90 \) +append \
\( -clone 0 -rotate 180 \) -append \
-write mpr:cell +delete \
-size ${width}x${height} tile:mpr:cell \
-background "$bgcolor" -flatten "$outfile"
fi
exit 0

302
scripts/magick/tiler Normal file
View file

@ -0,0 +1,302 @@
#!/bin/bash
#
# Developed by Fred Weinhaus 4/8/2011 .......... revised 5/3/2015
#
# ------------------------------------------------------------------------------
#
# Licensing:
#
# Copyright © Fred Weinhaus
#
# My scripts are available free of charge for non-commercial use, ONLY.
#
# For use of my scripts in commercial (for-profit) environments or
# non-free applications, please contact me (Fred Weinhaus) for
# licensing arrangements. My email address is fmw at alink dot net.
#
# If you: 1) redistribute, 2) incorporate any of these scripts into other
# free applications or 3) reprogram them in another scripting language,
# then you must contact me for permission, especially if the result might
# be used in a commercial or for-profit environment.
#
# My scripts are also subject, in a subordinate manner, to the ImageMagick
# license, which can be found at: http://www.imagemagick.org/script/license.php
#
# ------------------------------------------------------------------------------
#
####
#
# USAGE: tiler [-m method] [-o overlap] [-v vpmethod] infile outfile
# USAGE: tiler [-h or -help]
#
# OPTIONS:
#
# -m method method of overlap blending; choices are 1 or 2;
# 1 is simple average blending; 2 is ramped blending;
# default=1
# -o overlap seam overlap amount in pixels; integer>=0; default=2
# -v vpmethod virtual-pixel method used to extend the quadrants;
# any valid IM non-background virtual-pixel method
# is allowed. Best choices seem to be mirror or edge;
# default=mirror
#
# NOTE: the input image (infile) must be square and have even dimensions.
#
###
#
# NAME: TILER
#
# PURPOSE: To convert an image into a tilable texture.
#
# DESCRIPTION: TILER converts an image into a tilable texture. It does this
# by swapping diagonal quadrants, exending the borders to get overlap and
# then composite blending the extended quadrants. The extension and blending
# attempt to minimize or avoid manual painting/blurring/cloning along the
# seams.
#
# OPTIONS:
#
# -m method ... METHOD of overlap blending. The choices are 1 or 2. Method 1
# is a simple average blending. Method 2 is a ramped blending. The default=1.
#
# -o overlap ... OVERLAP is amount of extension of the quadrants in order to
# cover the center horizontal and vertical seems. Values are integers>=0.
# The default=2
#
# -v vpmethod ... VPMETHOD is the virtual-pixel method used to extend the
# quadrants. Any valid IM non-background virtual-pixel method is allowed.
# Recommended values are either mirror or edge. The default is mirror.
#
# Requirement: IM 6.5.3-4 or higher due to the use in method 1 of
# convert ... -compose blend -define compose:args=50,50 -composite ...
#
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
#
######
#
# set default values
method=1 # blending method
overlap=2 # overlap of quadrants
vp="mirror" # virtual pixel method for extending the quadrants
# set directory for temporary files
dir="." # suggestions are dir="." or dir="/tmp"
# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
PROGDIR=`dirname $PROGNAME` # extract directory of program
PROGNAME=`basename $PROGNAME` # base name of program
usage1()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}
# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
{
test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
[ $test -eq 1 ] && errMsg "$errorMsg"
}
# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 8 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameter values
case "$1" in
-h|-help) # help information
echo ""
usage2
exit 0
;;
-m) # get method
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID METHOD SPECIFICATION ---"
checkMinus "$1"
method=`expr "$1" : '\([0-2]*\)'`
[ "$method" = "" ] && errMsg "--- METHOD=$method MUST BE A NON-NEGATIVE INTEGER ---"
[ $method -ne 1 -a $method -ne 2 ] && errMsg "--- METHOD=$method MUST BE EITHER 1 OR 2 ---"
;;
-o) # get overlap
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID OVERLAP SPECIFICATION ---"
checkMinus "$1"
overlap=`expr "$1" : '\([0-9]*\)'`
[ "$overlap" = "" ] && errMsg "--- OVERLAP=$overlap MUST BE A NON-NEGATIVE INTEGER ---"
;;
-v) # get vpmethod
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID VPMETHOD SPECIFICATION ---"
checkMinus "$1"
vp="$1"
vp=`echo "$vp" | tr "[:upper:]" "[:lower:]"`
case "$vp" in
black) ;;
dither) ;;
edge) ;;
gray) ;;
mirror) ;;
random) ;;
tile) ;;
transparent) ;;
white) ;;
*) errMsg "--- VPMETHOD=$vp IS AN INVALID VALUE ---"
esac
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"
fi
# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
# set up temp files
tmpA1="$dir/tiler_A_$$.mpc"
tmpA2="$dir/tiler_A_$$.cache"
tmpM1="$dir/tiler_M_$$.mpc"
tmpM2="$dir/tiler_M_$$.cache"
tmpN1="$dir/tiler_N_$$.mpc"
tmpN2="$dir/tiler_N_$$.cache"
trap "rm -f $tmpA1 $tmpA2 $tmpM1 $tmpM2 $tmpN1 $tmpN2;" 0
trap "rm -f $tmpA1 $tmpA2 $tmpM1 $tmpM2 $tmpN1 $tmpN2; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpA2 $tmpM1 $tmpM2 $tmpN1 $tmpN2; exit 1" ERR
# read the input image into the tmp cached image.
convert -quiet "$infile" +repage "$tmpA1" ||
errMsg "--- FILE $infile NOT READABLE OR HAS ZERO SIZE ---"
# validate input image is square
wd=`convert $tmpA1 -ping -format "%w" info:`
ht=`convert $tmpA1 -ping -format "%h" info:`
test0=`convert xc: -format "%[fx:$wd==$ht?1:0]" info:`
[ $test0 -eq 0 ] && errMsg "--- FILE $infile IS NOT SQUARE ---"
test1=`convert xc: -format "%[fx:mod($wd,2)]" info:`
test2=`convert xc: -format "%[fx:mod($ht,2)]" info:`
[ $test1 -eq 1 -o $test2 -eq 1 ] && errMsg "--- FILE $infile DOES NOT HAVE EVEN DIMENSIONS ---"
# compute half width and other parameters
ww=`convert xc: -format "%[fx:$wd/2]" info:`
hh=`convert xc: -format "%[fx:$ht/2]" info:`
wwo=`convert xc: -format "%[fx:$ww+$overlap]" info:`
hho=`convert xc: -format "%[fx:$hh+$overlap]" info:`
rollamount="+${ww}+${hh}"
cropsize="${ww}x${hh}+0+0"
www=$(($ww+$overlap))
hhh=$(($hh+$overlap))
sesize="${www}x${hhh}+0+0"
swsize="${www}x${hhh}-$overlap+0"
nesize="${www}x${hhh}+0-$overlap"
nwsize="${www}x${hhh}-$overlap-$overlap"
gsize=$(($overlap + 2))
# process image
if [ $method -eq 1 ]; then
# test for valid IM version
im_version=`convert -list configure | \
sed '/^LIB_VERSION_NUMBER */!d; s//,/; s/,/,0/g; s/,0*\([0-9][0-9]\)/\1/g' | head -n 1`
[ "$im_version" -lt "06050304" ] && errMsg "--- REQUIRES IM VERSION 6.5.3-4 OR HIGHER ---"
# Do simple average blending in overlap
convert $tmpA1 \
\( -clone 0 -gravity southeast -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$sesize -distort SRT 0 +repage \) \
\( -clone 0 -gravity southwest -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$swsize -distort SRT 0 +repage \) \
\( -clone 0 -gravity northeast -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$nesize -distort SRT 0 +repage \) \
\( -clone 0 -gravity northwest -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$nwsize -distort SRT 0 +repage \) \
\( -clone 0 -roll $rollamount \
-clone 1 -gravity northwest -compose blend -define compose:args=50,50 -composite \
-clone 2 -gravity northeast -compose blend -define compose:args=50,50 -composite \
-clone 3 -gravity southwest -compose blend -define compose:args=50,50 -composite \
-clone 4 -gravity southeast -compose blend -define compose:args=50,50 -composite \) \
-delete 0-4 \
"$outfile"
elif [ $method -eq 2 ]; then
# Create horizontal mask; black on left, gradient, white on right
convert \( -size ${ww}x${hho} xc:black xc:white +append \) \
\( -size ${hho}x$gsize gradient: -rotate 90 \) -gravity center \
-compose over -composite +repage $tmpM1
# create vertical mask; black on top, gradient, white on bottom
convert \( -size ${wd}x${hh} xc:black xc:white -append \) \
\( -size ${wd}x$gsize gradient: -rotate 180 \) -gravity center \
-compose over -composite +repage $tmpN1
# Do ramped blending in overlap
convert $infile \
\( -clone 0 -gravity southeast -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$sesize -distort SRT 0 +repage \) \
\( -clone 0 -gravity southwest -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$swsize -distort SRT 0 +repage \) \
\( -clone 0 -gravity northeast -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$nesize -distort SRT 0 +repage \) \
\( -clone 0 -gravity northwest -crop $cropsize +repage -virtual-pixel $vp \
-define distort:viewport=$nwsize -distort SRT 0 +repage \) \
\( -clone 1 -gravity northwest -extent ${wd}x${hho} -clone 2 $tmpM1 \
-gravity northeast -composite \) \
\( -clone 3 -gravity southwest -extent ${wd}x${hho} -clone 4 $tmpM1 \
-gravity southeast -composite \) \
\( -clone 5 -gravity north -extent ${wd}x${ht} -clone 6 $tmpN1 \
-gravity south -composite \) \
-delete 0-6 \
"$outfile"
fi
exit 0

27
scripts/munge_color.ps1 Normal file
View file

@ -0,0 +1,27 @@
Param (
[string]$Colors=16,
[string]$Size="256x256",
[string]$Pixel="7"
)
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\armored_knight_1.png -OutFile .\assets\armored_knight_1-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\gold_savior_oil.png -OutFile .\assets\gold_savior_oil-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\ceiling_test.png -OutFile .\assets\ceiling_test-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\torch_pillar.png -OutFile .\assets\torch_pillar-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\torch_pillar.png -OutFile .\assets\torch_pillar-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\glowing_moss_wall.png -OutFile .\assets\glowing_moss_wall-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\large_stone_floor.png -OutFile .\assets\large_stone_floor-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\floor_tile_test.png -OutFile .\assets\floor_tile_test-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\wall_texture_test.png -OutFile .\assets\wall_texture_test-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\hairy_spider.png -OutFile .\assets\hairy_spider-256.png -Colors $Colors -Pixel $Pixel -Size $Size
.\scripts\pixelize.ps1 -InFile C:\Users\lcthw\Pictures\Games\axe_ranger.png -OutFile .\assets\axe_ranger-256.png -Colors $Colors -Pixel $Pixel -Size $Size

16
scripts/pixelize.ps1 Normal file
View file

@ -0,0 +1,16 @@
Param (
[string]$InFile,
[string]$OutFile,
[string]$Colors=16,
[string]$Size="256x256",
[string]$Pixel="7"
)
cp $InFile temp.png
# magick temp.png -colors $Colors -quantize sRGB -dither FloydSteinberg color.png
magick temp.png -posterize $Colors -quantize sRGB -dither FloydSteinberg poster.png
bash ./scripts/magick/pixelize -s $Pixel -m 2 poster.png pixels.png
magick.exe pixels.png -interpolate nearest -interpolative-resize $Size $OutFile

7
scripts/reset_build.ps1 Normal file
View file

@ -0,0 +1,7 @@
mv .\subprojects\packagecache .
rm -recurse -force .\subprojects\,.\builddir\
mkdir subprojects
mv .\packagecache .\subprojects\
mkdir builddir
cp wraps\*.wrap subprojects\
meson setup --default-library=static --prefer-static builddir

10
scripts/reset_build.sh Normal file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
mv -f ./subprojects/packagecache .
rm -rf subprojects builddir
mkdir subprojects
mv -f packagecache ./subprojects/ && true
mkdir builddir
cp wraps/*.wrap subprojects/
# on OSX you can't do this with static
meson setup --default-library=static --prefer-static builddir

BIN
scripts/win_installer.ifp Normal file

Binary file not shown.

214
src/ai/ai.cpp Normal file
View file

@ -0,0 +1,214 @@
#include "dbc.hpp"
#include "ai.hpp"
namespace ai {
using namespace nlohmann;
using namespace dbc;
static AIManager AIMGR;
static bool initialized = false;
inline void validate_profile(nlohmann::json& profile) {
for(auto& [name_key, value] : profile.items()) {
check(value < STATE_MAX,
$F("profile field {} has value {} greater than STATE_MAX {}",
(std::string)name_key, (int)value, STATE_MAX));
}
}
Action config_action(AIProfile& profile, nlohmann::json& config) {
check(config.contains("name"), "config_action: action config missing name");
check(config.contains("cost"), "config_action: action config missing cost");
Action result(config["name"], config["cost"]);
check(config.contains("needs"),
$F("config_action: no 'needs' field", result.name));
check(config.contains("effects"),
$F("config_action: no 'effects' field", result.name));
for(auto& [name_key, value] : config["needs"].items()) {
check(profile.contains(name_key), $F("config_action({}): profile does not have need named {}", result.name, name_key));
result.needs(profile.at(name_key), bool(value));
}
for(auto& [name_key, value] : config["effects"].items()) {
check(profile.contains(name_key), $F("config_action({}): profile does not have effect named {}", result.name, name_key));
result.effect(profile.at(name_key), bool(value));
}
return result;
}
State config_state(AIProfile& profile, nlohmann::json& config) {
State result;
for(auto& [name_key, value] : config.items()) {
check(profile.contains(name_key), $F("config_state: profile does not have name {}", name_key));
int name_id = profile.at(name_key);
result[name_id] = bool(value);
}
return result;
}
/*
* This is only used in tests so I can load different fixtures.
*/
void reset() {
initialized = false;
AIMGR.actions.clear();
AIMGR.states.clear();
AIMGR.scripts.clear();
AIMGR.profile = json({});
}
void init(const std::string& config_path) {
if(!initialized) {
auto config = settings::get(config_path);
// profile specifies what keys (bitset indexes) are allowed
// and how they map to the bitset of State
validate_profile(config["profile"]);
// relies on json conversion?
AIMGR.profile = config["profile"];
// load all actions
auto& actions = config["actions"];
for(auto& action_vars : actions) {
auto the_action = config_action(AIMGR.profile, action_vars);
AIMGR.actions.insert_or_assign(the_action.name, the_action);
}
// load all states
auto& states = config["states"];
for(auto& [name, state_vars] : states.items()) {
auto the_state = config_state(AIMGR.profile, state_vars);
AIMGR.states.insert_or_assign(name, the_state);
}
auto& scripts = config["scripts"];
for(auto& [script_name, action_names] : scripts.items()) {
std::vector<Action> the_script;
for(auto name : action_names) {
check(AIMGR.actions.contains(name),
$F("ai::init(): script {} uses action {} that doesn't exist",
(std::string)script_name, (std::string)name));
the_script.push_back(AIMGR.actions.at(name));
}
AIMGR.scripts.insert_or_assign(script_name, the_script);
}
initialized = true;
} else {
// BUG: should track the inits per file, or create a separate init that's an overload of the default
// then this version is just used in tests
dbc::sentinel($F("DOUBLE INIT {}: AI manager should only be intialized once if not in tests.", config_path));
}
}
void check_valid_action(std::string name, std::string msg) {
dbc::check(AIMGR.actions.contains(name),
$F("{} tried to access action that doesn't exist {}",
msg, name));
}
State load_state(std::string state_name) {
check(initialized, "you forgot to initialize the AI first.");
check(AIMGR.states.contains(state_name),
$F("ai::load_state({}): state does not exist in config",
state_name));
return AIMGR.states.at(state_name);
}
Action load_action(std::string action_name) {
check(initialized, "you forgot to initialize the AI first.");
check(AIMGR.actions.contains(action_name),
$F("ai::load_action({}): action does not exist in config",
action_name));
return AIMGR.actions.at(action_name);
}
std::vector<Action> load_script(std::string script_name) {
check(AIMGR.scripts.contains(script_name),
$F("ai::load_script(): no script named {} configured", script_name));
return AIMGR.scripts.at(script_name);
}
ActionPlan plan(std::string script_name, State start, State goal) {
// BUG: could probably memoize here, since:
// same script+same start+same goal will/should produce the same results
check(initialized, "you forgot to initialize the AI first.");
auto script = load_script(script_name);
return plan_actions(script, start, goal);
}
int state_id(std::string name) {
check(AIMGR.profile.contains(name),
$F("ai::state_id({}): id is not configured in profile", name));
return AIMGR.profile.at(name);
}
void set(State& state, std::string name, bool value) {
// resort by best fit
state.set(state_id(name), value);
}
bool test(State state, std::string name) {
return state.test(state_id(name));
}
void EntityAI::fit_sort() {
if(active()) {
std::sort(plan.script.begin(), plan.script.end(),
[&](auto& l, auto& r) {
int l_cost = l.cost + ai::distance_to_goal(start, goal);
int r_cost = r.cost + ai::distance_to_goal(start, goal);
return l_cost < r_cost;
});
}
}
std::string& EntityAI::wants_to() {
return plan.script[0].name;
}
bool EntityAI::wants_to(std::string name) {
ai::check_valid_action(name, "EntityAI::wants_to");
return plan.script.size() > 0 && plan.script[0].name == name;
}
bool EntityAI::active() {
if(plan.script.size() == 1) {
return plan.script[0] != FINAL_ACTION;
} else {
return plan.script.size() != 0;
}
}
void EntityAI::set_state(std::string name, bool setting) {
fit_sort();
ai::set(start, name, setting);
}
bool EntityAI::get_state(std::string name) {
return ai::test(start, name);
}
void EntityAI::update() {
plan = ai::plan(script, start, goal);
fit_sort();
}
AIProfile* profile() {
return &AIMGR.profile;
}
}

65
src/ai/ai.hpp Normal file
View file

@ -0,0 +1,65 @@
#pragma once
#include <vector>
#include "algos/matrix.hpp"
#include <bitset>
#include <limits>
#include <optional>
#include <nlohmann/json.hpp>
#include "game/config.hpp"
#include "goap.hpp"
namespace ai {
struct EntityAI {
std::string script;
ai::State start;
ai::State goal;
ai::ActionPlan plan;
EntityAI(std::string script, ai::State start, ai::State goal) :
script(script), start(start), goal(goal)
{
}
EntityAI() {};
bool wants_to(std::string name);
std::string& wants_to();
void fit_sort();
bool active();
void set_state(std::string name, bool setting);
bool get_state(std::string name);
void update();
void dump();
std::string to_string();
};
struct AIManager {
AIProfile profile;
std::unordered_map<std::string, Action> actions;
std::unordered_map<std::string, State> states;
std::unordered_map<std::string, std::vector<Action>> scripts;
};
/* This is really only used in test to load different fixtures. */
void reset();
void init(const std::string& config_path);
Action config_action(AIProfile& profile, nlohmann::json& config);
State config_state(AIProfile& profile, nlohmann::json& config);
int state_id(std::string name);
State load_state(std::string state_name);
Action load_action(std::string action_name);
std::vector<Action> load_script(std::string script_name);
void set(State& state, std::string name, bool value=true);
bool test(State state, std::string name);
ActionPlan plan(std::string script_name, State start, State goal);
/* Mostly used for debugging and validation. */
void check_valid_action(std::string name, std::string msg);
}

74
src/ai/ai_debug.cpp Normal file
View file

@ -0,0 +1,74 @@
#include "ai.hpp"
#include "ai_debug.hpp"
namespace ai {
/*
* Yeah this is weird but it's only to debug things like
* the preconditions which are weirdly done.
*/
void dump_only(State state, bool matching, bool show_as) {
AIProfile* profile = ai::profile();
for(auto& [name, name_id] : *profile) {
if(state.test(name_id) == matching) {
fmt::println("\t{}={}", name, show_as);
}
}
}
void dump_state(State state) {
AIProfile* profile = ai::profile();
for(auto& [name, name_id] : *profile) {
fmt::println("\t{}={}", name,
state.test(name_id));
}
}
void dump_action(Action& action) {
fmt::println(" --ACTION: {}, cost={}", action.name, action.cost);
fmt::println(" PRECONDS:");
dump_only(action.$positive_preconds, true, true);
dump_only(action.$negative_preconds, true, false);
fmt::println(" EFFECTS:");
dump_only(action.$positive_effects, true, true);
dump_only(action.$negative_effects, true, false);
}
State dump_script(std::string msg, State start, Script& script) {
fmt::println("--SCRIPT DUMP: {}", msg);
fmt::println("# STATE BEFORE:");
dump_state(start);
fmt::print("% ACTIONS PLANNED:");
for(auto& action : script) {
fmt::print("{} ", action.name);
}
fmt::print("\n");
for(auto& action : script) {
dump_action(action);
start = action.apply_effect(start);
fmt::println(" ## STATE AFTER:");
dump_state(start);
}
return start;
}
void EntityAI::dump() {
dump_script(script, start, plan.script);
}
std::string EntityAI::to_string() {
AIProfile* profile = ai::profile();
std::string result = wants_to();
for(auto& [name, name_id] : *profile) {
result += fmt::format("\n{}={}", name, start.test(name_id));
}
return result;
}
}

10
src/ai/ai_debug.hpp Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#include "goap.hpp"
namespace ai {
AIProfile* profile();
void dump_only(State state, bool matching, bool show_as);
void dump_state(State state);
void dump_action(Action& action);
State dump_script(std::string msg, State start, Script& script);
}

187
src/ai/goap.cpp Normal file
View file

@ -0,0 +1,187 @@
#include "dbc.hpp"
#include "goap.hpp"
#include "ai_debug.hpp"
#include "algos/stats.hpp"
#include <queue>
// #define DEBUG_CYCLES 1
namespace ai {
using namespace nlohmann;
using namespace dbc;
bool is_subset(State& source, State& target) {
State result = source & target;
return result == target;
}
void Action::needs(int name, bool val) {
if(val) {
$positive_preconds[name] = true;
$negative_preconds[name] = false;
} else {
$negative_preconds[name] = true;
$positive_preconds[name] = false;
}
}
void Action::effect(int name, bool val) {
if(val) {
$positive_effects[name] = true;
$negative_effects[name] = false;
} else {
$negative_effects[name] = true;
$positive_effects[name] = false;
}
}
void Action::ignore(int name) {
$positive_preconds[name] = false;
$negative_preconds[name] = false;
}
bool Action::can_effect(State& state) {
bool posbit_match = (state & $positive_preconds) == $positive_preconds;
bool negbit_match = (state & $negative_preconds) == ALL_ZERO;
return posbit_match && negbit_match;
}
State Action::apply_effect(State& state) {
return (state | $positive_effects) & ~$negative_effects;
}
int distance_to_goal(State from, State to) {
auto result = from ^ to;
int count = result.count();
return count;
}
inline void dump_came_from(std::string msg, std::unordered_map<Action, Action>& came_from, Action& current) {
fmt::println("{}: {}", msg, current.name);
for(auto& [from, to] : came_from) {
fmt::println("from={}; to={}", from.name, to.name);
}
}
inline void path_invariant(std::unordered_map<Action, Action>& came_from, Action current) {
#if defined(DEBUG_CYCLES)
bool final_found = current == FINAL_ACTION;
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
current = came_from.at(current);
final_found = current == FINAL_ACTION;
}
if(!final_found) {
dump_came_from("CYCLE DETECTED!", came_from, current);
dbc::sentinel("AI CYCLE FOUND!");
}
#else
(void)came_from; // disable errors about unused
(void)current;
#endif
}
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
Script total_path{current};
path_invariant(came_from, current);
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
auto next = came_from.at(current);
if(next != FINAL_ACTION) {
// remove the previous node to avoid cycles and repeated actions
total_path.push_front(next);
came_from.erase(current);
current = next;
} else {
// found the terminator, done
break;
}
}
return total_path;
}
inline int h(State start, State goal) {
return distance_to_goal(start, goal);
}
inline int d(State start, State goal) {
return distance_to_goal(start, goal);
}
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
check(!open_set.empty(), "open set can't be empty in find_lowest");
int found_score = std::numeric_limits<int>::max();
ActionState found_as;
for(auto& kv : open_set) {
if(kv.second <= found_score) {
found_score = kv.second;
found_as = kv.first;
}
}
return found_as;
}
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal) {
std::unordered_map<ActionState, int> open_set;
std::unordered_map<Action, Action> came_from;
std::unordered_map<State, int> g_score;
std::unordered_map<State, bool> closed_set;
ActionState current{FINAL_ACTION, start};
g_score.insert_or_assign(start, 0);
open_set.insert_or_assign(current, h(start, goal));
while(!open_set.empty()) {
// current := the node in openSet having the lowest fScore[] value
current = find_lowest(open_set);
if(is_subset(current.state, goal)) {
return {true,
reconstruct_path(came_from, current.action)};
}
open_set.erase(current);
closed_set.insert_or_assign(current.state, true);
for(auto& neighbor_action : actions) {
// calculate the State being current/neighbor
if(!neighbor_action.can_effect(current.state)) continue;
auto neighbor = neighbor_action.apply_effect(current.state);
if(closed_set.contains(neighbor)) continue;
// BUG: no matter what I do cost really doesn't impact the graph
// Additionally, every other GOAP implementation has the same problem, and
// it's probably because the selection of actions is based more on sets matching
// than actual weights of paths. This reduces the probability that an action will
// be chosen over another due to only cost.
int d_score = d(current.state, neighbor) + neighbor_action.cost;
int tentative_g_score = g_score[current.state] + d_score;
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
if(tentative_g_score + neighbor_action.cost < neighbor_g_score) {
came_from.insert_or_assign(neighbor_action, current.action);
g_score.insert_or_assign(neighbor, tentative_g_score);
ActionState neighbor_as{neighbor_action, neighbor};
int score = tentative_g_score + h(neighbor, goal);
// this maybe doesn't need score
open_set.insert_or_assign(neighbor_as, score);
}
}
}
return {is_subset(current.state, goal), reconstruct_path(came_from, current.action)};
}
}

84
src/ai/goap.hpp Normal file
View file

@ -0,0 +1,84 @@
#pragma once
#include <vector>
#include "algos/matrix.hpp"
#include <bitset>
#include <limits>
#include <optional>
#include <nlohmann/json.hpp>
#include "game/config.hpp"
namespace ai {
// ZED: I don't know if this is the best place for this
using AIProfile = std::unordered_map<std::string, int>;
constexpr const int SCORE_MAX = std::numeric_limits<int>::max() / 2;
constexpr const size_t STATE_MAX = 32;
using State = std::bitset<STATE_MAX>;
const State ALL_ZERO;
const State ALL_ONES = ~ALL_ZERO;
struct Action {
std::string name="";
int cost = 0;
State $positive_preconds=ALL_ZERO;
State $negative_preconds=ALL_ZERO;
State $positive_effects=ALL_ZERO;
State $negative_effects=ALL_ZERO;
void needs(int name, bool val);
void effect(int name, bool val);
void ignore(int name);
bool can_effect(State& state);
State apply_effect(State& state);
bool operator==(const Action& other) const {
return other.name == name;
}
};
using Script = std::deque<Action>;
const Action FINAL_ACTION{"END", SCORE_MAX};
struct ActionState {
Action action;
State state;
ActionState(Action action, State state) :
action(action), state(state) {}
ActionState() : action(FINAL_ACTION), state(0) {}
bool operator==(const ActionState& other) const {
return other.action == action && other.state == state;
}
};
struct ActionPlan {
bool complete = false;
Script script;
};
bool is_subset(State& source, State& target);
int distance_to_goal(State from, State to);
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
}
template<> struct std::hash<ai::Action> {
size_t operator()(const ai::Action& p) const {
return std::hash<std::string>{}(p.name);
}
};
template<> struct std::hash<ai::ActionState> {
size_t operator()(const ai::ActionState& p) const {
return std::hash<ai::Action>{}(p.action) ^ std::hash<ai::State>{}(p.state);
}
};

247
src/algos/dinkyecs.hpp Normal file
View file

@ -0,0 +1,247 @@
#pragma once
#include "dbc.hpp"
#include <any>
#include <functional>
#include <queue>
#include <tuple>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <optional>
#include <memory>
namespace DinkyECS
{
using Entity = unsigned long;
const Entity NONE = 0;
template <typename T>
struct ComponentStorage {
std::vector<T> data;
};
struct Event {
int event = 0;
Entity entity = 0;
std::any data;
};
using EntityMap = std::unordered_map<Entity, size_t>;
using EventQueue = std::queue<Event>;
using TypeMap = std::unordered_map<std::type_index, std::any>;
struct World {
unsigned long entity_count = NONE+1;
std::unordered_map<std::type_index, EntityMap> $components;
std::shared_ptr<TypeMap> $facts = nullptr;
std::unordered_map<std::type_index, EventQueue> $events;
std::unordered_map<std::type_index, std::any> $component_storages;
std::unordered_map<std::type_index, std::queue<size_t>> $free_indices;
std::unordered_map<Entity, bool> $constants;
World() : $facts(std::make_shared<TypeMap>())
{}
Entity entity() { return ++entity_count; }
void destroy(DinkyECS::Entity entity) {
dbc::check(!$constants.contains(entity), "trying to destroy an entity in constants");
for(auto& [tid, map] : $components) {
if(map.contains(entity)) {
size_t index = map.at(entity);
auto& free_queue = $free_indices.at(tid);
free_queue.push(index);
map.erase(entity);
}
}
}
void clone_into(DinkyECS::World &to_world) {
to_world.$constants = $constants;
to_world.$facts = $facts;
// BUG*10: entity IDs should be a global counter, not per world
to_world.entity_count = entity_count;
to_world.$component_storages = $component_storages;
for(auto [eid, is_set] : $constants) {
dbc::check(is_set == true, "is_set was not true? WHAT?!");
dbc::check(eid <= entity_count,
$F("eid {} is not less than entity_count {}", eid, entity_count));
for(const auto &[tid, eid_map] : $components) {
auto &their_map = to_world.$components[tid];
if(eid_map.contains(eid)) {
their_map.insert_or_assign(eid, eid_map.at(eid));
}
}
}
}
void make_constant(DinkyECS::Entity entity) {
$constants.try_emplace(entity, true);
}
void not_constant(DinkyECS::Entity entity) {
$constants.erase(entity);
}
template <typename Comp>
size_t make_component() {
auto &storage = component_storage_for<Comp>();
auto &free_queue = $free_indices.at(std::type_index(typeid(Comp)));
size_t index;
if(!free_queue.empty()) {
index = free_queue.front();
free_queue.pop();
} else {
storage.data.emplace_back();
index = storage.data.size() - 1;
}
return index;
}
template <typename Comp>
ComponentStorage<Comp> &component_storage_for() {
auto type_index = std::type_index(typeid(Comp));
$component_storages.try_emplace(type_index, ComponentStorage<Comp>{});
$free_indices.try_emplace(type_index, std::queue<size_t>{});
return std::any_cast<ComponentStorage<Comp> &>(
$component_storages.at(type_index));
}
template <typename Comp>
EntityMap &entity_map_for() {
return $components[std::type_index(typeid(Comp))];
}
template <typename Comp>
EventQueue &queue_map_for() {
return $events[std::type_index(typeid(Comp))];
}
template <typename Comp>
void remove(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
if(map.contains(ent)) {
size_t index = map.at(ent);
auto& free_queue = $free_indices.at(std::type_index(typeid(Comp)));
free_queue.push(index);
map.erase(ent);
}
}
template <typename Comp>
void set_the(Comp val) {
$facts->insert_or_assign(std::type_index(typeid(Comp)), val);
}
template <typename Comp>
Comp &get_the() {
auto comp_id = std::type_index(typeid(Comp));
dbc::check($facts->contains(comp_id),
$F("!!!! ATTEMPT to access world fact that hasn't been set yet: {}",
typeid(Comp).name()));
// use .at to get std::out_of_range if fact not set
std::any &res = $facts->at(comp_id);
return std::any_cast<Comp &>(res);
}
template <typename Comp>
bool has_the() {
auto comp_id = std::type_index(typeid(Comp));
return $facts->contains(comp_id);
}
template <typename Comp>
void set(Entity ent, Comp val) {
EntityMap &map = entity_map_for<Comp>();
if(has<Comp>(ent)) {
get<Comp>(ent) = val;
return;
}
map.insert_or_assign(ent, make_component<Comp>());
get<Comp>(ent) = val;
}
template <typename Comp>
Comp &get(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
auto index = map.at(ent);
return storage.data[index];
}
template <typename Comp>
bool has(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
return map.contains(ent);
}
template <typename Comp>
void query(std::function<void(Entity, Comp &)> cb) {
EntityMap &map = entity_map_for<Comp>();
for(auto &[entity, index] : map) {
cb(entity, get<Comp>(entity));
}
}
template <typename CompA, typename CompB>
void query(std::function<void(Entity, CompA &, CompB &)> cb) {
EntityMap &map_a = entity_map_for<CompA>();
EntityMap &map_b = entity_map_for<CompB>();
for(auto &[entity, index_a] : map_a) {
if(map_b.contains(entity)) {
cb(entity, get<CompA>(entity), get<CompB>(entity));
}
}
}
template <typename Comp>
void send(Comp event, Entity entity, std::any data) {
EventQueue &queue = queue_map_for<Comp>();
queue.push({event, entity, data});
}
template <typename Comp>
Event recv() {
EventQueue &queue = queue_map_for<Comp>();
Event evt = queue.front();
queue.pop();
return evt;
}
template <typename Comp>
bool has_event() {
EventQueue &queue = queue_map_for<Comp>();
return !queue.empty();
}
/* std::optional can't do references. Don't try it!
* Actually, this sucks, either delete it or have it
* return pointers (assuming optional can handle pointers)
*/
template <typename Comp>
Comp* get_if(DinkyECS::Entity entity) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
if(map.contains(entity)) {
auto index = map.at(entity);
return &storage.data[index];
} else {
return nullptr;
}
}
};
} // namespace DinkyECS

44
src/algos/matrix.cpp Normal file
View file

@ -0,0 +1,44 @@
#include "algos/matrix.hpp"
#include <fmt/core.h>
#include "constants.hpp"
using namespace fmt;
using std::min, std::max;
namespace matrix {
void dump(const std::string &msg, Matrix &map, int show_x, int show_y) {
println("----------------- {}", msg);
for(each_row it{map}; it.next();) {
int cell = map[it.y][it.x];
if(int(it.x) == show_x && int(it.y) == show_y) {
if(cell == WALL_PATH_LIMIT) {
print("!<", cell);
} else {
print("{:x}<", cell);
}
} else if(cell == DEAD_END_VALUE) {
print("* ");
} else if(cell == DOOR_VALUE) {
print("¦ ");
} else if(cell == 1) {
print("▒▒");
} else if(cell == ROOM_SPACE_VALUE) {
print("\%\%");
} else if(cell == WALL_PATH_LIMIT) {
print("■■");
} else if(cell == 0) {
print(" ");
} else if(cell > 15 && cell < 32) {
print("{:X} ", cell - 16);
} else if(cell > 31) {
print("..");
} else {
print("{:x} ", cell);
}
if(it.row) print("\n");
}
}
}

44
src/algos/matrix.hpp Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include "algos/shiterator.hpp"
namespace matrix {
using Row = shiterator::BaseRow<int>;
using Matrix = shiterator::Base<int>;
using viewport = shiterator::viewport_t<Matrix>;
using perimeter = shiterator::perimeter_t<Matrix>;
using each_cell = shiterator::each_cell_t<Matrix>;
using each_row = shiterator::each_row_t<Matrix>;
using box = shiterator::box_t<Matrix>;
using compass = shiterator::compass_t<Matrix>;
using circle = shiterator::circle_t<Matrix>;
using rectangle = shiterator::rectangle_t<Matrix>;
using rando_rect = shiterator::rando_rect_t<Matrix>;
using rando_rect = shiterator::rando_rect_t<Matrix>;
using rando_box = shiterator::rando_box_t<Matrix>;
using line = shiterator::line_t;
void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1);
inline Matrix make(size_t width, size_t height) {
return shiterator::make<int>(width, height);
}
inline bool inbounds(Matrix &mat, size_t x, size_t y) {
return shiterator::inbounds(mat, x, y);
}
inline size_t width(Matrix &mat) {
return shiterator::width(mat);
}
inline size_t height(Matrix &mat) {
return shiterator::height(mat);
}
inline void assign(Matrix &out, int new_value) {
shiterator::assign(out, new_value);
}
}

509
src/algos/maze.cpp Normal file
View file

@ -0,0 +1,509 @@
#include <fmt/core.h>
#include <string>
#include "algos/rand.hpp"
#include "constants.hpp"
#include "algos/maze.hpp"
#include <algorithm>
using std::string;
using matrix::Matrix;
namespace maze {
inline bool complete(Matrix& maze, size_t width, size_t height) {
for(size_t row = 1; row < height; row += 2) {
for(size_t col = 1; col < width; col += 2) {
if(maze[row][col] != 0) return false;
}
}
return true;
}
std::vector<Point> neighborsAB(Matrix& maze, Point on) {
std::vector<Point> result;
std::array<Point, 4> points{{
{on.x, on.y - 2},
{on.x, on.y + 2},
{on.x - 2, on.y},
{on.x + 2, on.y}
}};
for(auto point : points) {
if(matrix::inbounds(maze, point.x, point.y)) {
result.push_back(point);
}
}
return result;
}
std::vector<Point> neighbors(Matrix& maze, Point on) {
std::vector<Point> result;
std::array<Point, 4> points{{
{on.x, on.y - 2},
{on.x, on.y + 2},
{on.x - 2, on.y},
{on.x + 2, on.y}
}};
for(auto point : points) {
if(matrix::inbounds(maze, point.x, point.y)) {
if(maze[point.y][point.x] == WALL_VALUE) {
result.push_back(point);
}
}
}
return result;
}
inline std::pair<Point, Point> find_coord(Matrix& maze, size_t width, size_t height) {
for(size_t y = 1; y < height; y += 2) {
for(size_t x = 1; x < width; x += 2) {
if(maze[y][x] == WALL_VALUE) {
auto found = neighborsAB(maze, {x, y});
for(auto point : found) {
if(maze[point.y][point.x] == 0) {
return {{x, y}, point};
}
}
}
}
}
matrix::dump("BAD MAZE", maze);
dbc::sentinel("failed to find coord?");
}
bool Builder::room_should_exist(Room& room, bool allow_dupes) {
// padding by 1 for the perimeter wall
if(!matrix::inbounds($walls, room.x - 1, room.y - 1) ||
!matrix::inbounds($walls, room.x + room.width + 2, room.y + room.height + 2))
{
return false;
}
if(room.overlaps($no_rooms_region)) {
return false;
}
for(auto& other : $rooms) {
bool is_duped = allow_dupes ? room == other : room != other;
if(is_duped && room.overlaps(other)) {
return false;
}
}
// it's in the map and doesn't collide with another room
return true;
}
inline size_t room_sweetspot(size_t width, size_t room_size) {
if(width < 20) {
return 4;
} else if(width < 30) {
return (width / room_size / 2);
} if(width < 50) {
return width / room_size;
} else {
return width / 2;
}
}
void Builder::randomize_rooms(size_t room_size) {
std::shuffle($dead_ends.begin(), $dead_ends.end(), Random::GENERATOR);
size_t max_rooms = room_sweetspot($width, room_size);
// use those dead ends to randomly place rooms
for(auto at : $dead_ends) {
// don't bother with dead ends near the edge
if(at.x < 2 || at.y < 2 || at.x > $width - 3 || at.y > $height - 3) continue;
// quit after we've hit the room max threshold
if($rooms.size() > max_rooms) break;
// get the room corners randomized
std::array<Point, 4> starts{{
{at.x, at.y}, // top left
{at.x - room_size + 1, at.y}, // top right
{at.x - room_size + 1, at.y - room_size + 1}, // bottom right
{at.x, at.y - room_size + 1} // bottom left
}};
size_t offset = Random::uniform(0, 3);
// BUG: this still accidentally merges rooms
for(size_t i = 0; i < starts.size(); i++) {
size_t index = (i + offset) % starts.size();
Room cur{starts[index].x, starts[index].y, room_size, room_size};
// if it's out of bounds skip it
if(room_should_exist(cur)) {
$rooms.push_back(cur);
break;
}
}
}
}
void Builder::clear() {
matrix::assign($walls, WALL_VALUE);
}
void Builder::divide(Point start, Point end) {
for(matrix::line it{start, end}; it.next();) {
$walls[it.y][it.x] = 0;
$walls[it.y+1][it.x] = 0;
}
}
void Builder::hunt_and_kill(Point on) {
if($rooms.size() > 0) place_rooms();
while(!complete($walls, $width, $height)) {
auto n = neighbors($walls, on);
if(n.size() == 0) {
// no neighbors, must be dead end
add_dead_end(on);
auto t = find_coord($walls, $width, $height);
on = t.first;
$walls[on.y][on.x] = 0;
size_t row = (on.y + t.second.y) / 2;
size_t col = (on.x + t.second.x) / 2;
$walls[row][col] = 0;
} else {
// found neighbors, pick random one
auto nb = n[Random::abs(size_t(0), n.size() - 1)];
$walls[nb.y][nb.x] = 0;
size_t row = (nb.y + on.y) / 2;
size_t col = (nb.x + on.x) / 2;
$walls[row][col] = 0;
on = nb;
}
}
if($rooms.size() > 0) place_rooms();
}
void Builder::place_rooms() {
for(auto& room : $rooms) {
for(matrix::rectangle it{$walls, room.x, room.y, room.width, room.height}; it.next();) {
$walls[it.y][it.x] = SPACE_VALUE;
}
}
}
void Builder::inner_donut(float outer_rad, float inner_rad) {
size_t x = $width / 2;
size_t y = $height / 2;
for(matrix::circle it{$walls, {x, y}, outer_rad}; it.next();)
{
for(int x = it.left; x < it.right; x++) {
$walls[it.y][x] = 0;
}
}
for(matrix::circle it{$walls, {x, y}, inner_rad}; it.next();)
{
for(int x = it.left; x < it.right; x++) {
$walls[it.y][x] = 1;
}
}
}
void Builder::remove_dead_ends() {
dbc::check($dead_ends.size() > 0, "you have to run an algo first, no dead_ends to remove");
for(auto at : $dead_ends) {
for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
if($walls[it.y][it.x] == SPACE_VALUE) {
punch_dead_end(at.x, at.y, it.x, it.y);
break;
}
}
}
}
void Builder::dump(const std::string& msg, bool path_too) {
Matrix wall_copy = $walls;
// mark the rooms too, but not if pathing
if(!path_too) {
for(auto& room : $rooms) {
for(matrix::rectangle it{wall_copy, room.x, room.y, room.width, room.height};
it.next();)
{
if(wall_copy[it.y][it.x] == 0) {
wall_copy[it.y][it.x] = ROOM_SPACE_VALUE;
}
}
}
}
// mark dead ends
for(auto at : $dead_ends) {
// don't mark dead ends if there's something else there
wall_copy[at.y][at.x] = DEAD_END_VALUE;
}
for(auto [at, _] : $doors) {
wall_copy[at.y][at.x] = DOOR_VALUE;
}
if(path_too) {
for(matrix::each_cell it{wall_copy}; it.next();) {
if(wall_copy[it.y][it.x] == SPACE_VALUE) {
wall_copy[it.y][it.x] = $pathing.$paths[it.y][it.x];
}
}
}
matrix::dump(msg, wall_copy);
}
void Builder::enclose() {
for(matrix::perimeter it{0, 0, $width, $height}; it.next();) {
$walls[it.y][it.x] = WALL_VALUE;
Point at{it.x,it.y};
if($doors.contains(at)) $doors.erase(at);
}
}
void Builder::open_box(size_t outer_size) {
size_t center_x = $width / 2;
size_t center_y = $height / 2;
// compensate for the box's border now
outer_size++;
// this can't be right but for now it's working
size_t x = center_x - outer_size;
size_t y = center_y - outer_size;
// BUG: is the + 1 here because the bug in perimeter
size_t width = (outer_size * 2) + 1;
size_t height = (outer_size * 2) + 1;
for(matrix::perimeter p{x, y, width, height}; p.next();) {
for(matrix::compass it{$walls, p.x, p.y}; it.next();) {
if($ends_map.contains({it.x, it.y})) {
$walls[y][x] = 0;
break;
}
}
}
}
bool Builder::valid_door(size_t x, size_t y) {
return (space_available(x, y - 1) && space_available(x, y + 1)) // north south
|| (space_available(x - 1, y) && space_available(x + 1, y));
}
void Builder::place_doors() {
for(auto room : $rooms) {
int best_longest = 0;
Point best_door{0,0};
bool door_found = false;
// do an initial pathing check and if it's good then done
int longest = compute_paths(room.x, room.y);
if(longest < WALL_PATH_LIMIT && longest > int($width * 2)) continue;
// can't path out of the room, so now punch a hole until it can
matrix::perimeter it{room.x - 1, room.y - 1, room.width + 2, room.height + 2};
while(it.next()) {
if($walls[it.y][it.x] == WALL_VALUE) {
// valid doors are free north/south or east/west
if(!valid_door(it.x, it.y)) continue;
$walls[it.y][it.x] = SPACE_VALUE;
longest = compute_paths(room.x, room.y);
// keep track of the best door so far, which is the one with the longest path
if(longest != WALL_PATH_LIMIT && longest > best_longest) {
best_longest = longest;
best_door = {it.x, it.y};
}
$walls[it.y][it.x] = WALL_VALUE;
}
dbc::check(best_longest < WALL_PATH_LIMIT, "bad best_longest!");
if(best_longest > int($width * 2)) break;
}
// should now have a door with the longest path
$walls[best_door.y][best_door.x] = SPACE_VALUE;
$doors.insert_or_assign(best_door, true);
}
}
void Builder::inner_box(size_t outer_size, size_t inner_size) {
size_t x = matrix::width($walls) / 2;
size_t y = matrix::height($walls) / 2;
for(matrix::box it{$walls, x, y, outer_size}; it.next();)
{
$walls[it.y][it.x] = 0;
}
for(matrix::box it{$walls, x, y, inner_size}; it.next();)
{
$walls[it.y][it.x] = 1;
}
// make a fake room that blocks others
$no_rooms_region = {x - outer_size, y - outer_size, outer_size * 2 + 1, outer_size * 2 + 1};
}
void Builder::add_dead_end(Point at) {
// doing this ensures no dupes, if it's !inserted then it already existed
auto [_, inserted] = $ends_map.insert_or_assign(at, true);
// so skip it, it isn't new
if(inserted) {
$dead_ends.push_back(at);
}
}
int Builder::compute_paths(size_t x, size_t y) {
Point test{x, y};
$pathing.set_target(test);
int longest = $pathing.compute_paths($walls);
// $pathing.dump("AFTER COMPUTE");
$pathing.clear_target(test);
return longest;
}
bool Builder::validate() {
size_t width = matrix::width($walls);
size_t height = matrix::height($walls);
// no rooms can overlap
for(auto& room : $rooms) {
if(!room_should_exist(room)) return false;
}
for(matrix::perimeter it{0, 0, width, height}; it.next();) {
if($ends_map.contains({it.x, it.y})) return false;
if($doors.contains({it.x, it.y})) return false;
if($walls[it.y][it.x] != WALL_VALUE) return false;
}
if($rooms.size() <= 1) return true;
// initial path test can just use one room then look for
// any cells that are empty in the walls map but unpathed in the paths
Room test_room = $rooms.at(Random::abs(size_t(0), $rooms.size() - 1));
compute_paths(test_room.x, test_room.y);
for(matrix::each_cell it{$walls}; it.next();) {
if($walls[it.y][it.x] == SPACE_VALUE &&
$pathing.$paths[it.y][it.x] == WALL_PATH_LIMIT) {
return false;
}
}
return true;
}
bool Builder::space_available(size_t x, size_t y) {
return (matrix::inbounds($walls, x, y) && // in bounds
$walls[y][x] == SPACE_VALUE && // is a space
!$doors.contains({x, y}) && // no door there
(x > 0 && y > 0 && x < $width - 2 && y < $height - 2)); // not perimeter;
}
void Builder::punch_dead_end(size_t at_x, size_t at_y, size_t x, size_t y) {
int diff_x = at_x - x;
int diff_y = at_y - y;
$walls[at_y + diff_y][at_x + diff_x] = SPACE_VALUE;
}
bool Builder::repair() {
enclose();
// if it's already valid then done
if(validate()) return true;
std::vector<Point> removed_ends;
bool now_valid = false;
for(auto& at : $dead_ends) {
// punch a hole for this dead end
for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
if($walls[it.y][it.x] == SPACE_VALUE) {
punch_dead_end(at.x, at.y, it.x, it.y);
removed_ends.push_back({it.x, it.y});
break;
}
}
// if that validates it then done
if(validate()) {
now_valid = true;
break;
}
}
// now go back and see if we can add any back
for(auto& at : removed_ends) {
auto temp = $walls[at.y][at.x];
$walls[at.y][at.x] = WALL_VALUE;
if(!validate()) {
$walls[at.y][at.x] = temp;
}
}
enclose();
return validate();
}
std::pair<Builder, bool> script(Map& map, nlohmann::json& config) {
maze::Builder maze(map);
for(auto& action : config) {
std::string aname = action["action"];
if(aname == "hunt_and_kill") {
maze.hunt_and_kill();
} else if(aname == "clear") {
maze.clear();
} else if(aname == "inner_box") {
std::vector<int> data = action["data"];
maze.inner_box(data[0], data[1]);
} else if(aname == "randomize_rooms") {
std::vector<int> data = action["data"];
maze.randomize_rooms(data[0]);
} else if(aname == "open_box") {
std::vector<int> data = action["data"];
maze.open_box(data[0]);
} else if(aname == "place_doors") {
maze.place_doors();
} else if(aname == "divide") {
std::vector<size_t> data = action["data"];
maze.divide({data[0], data[1]}, {data[2], data[3]});
} else if(aname == "inner_donut") {
std::vector<float> data = action["data"];
maze.inner_donut(data[0], data[1]);
} else if(aname == "remove_dead_ends") {
maze.remove_dead_ends();
} else if(aname == "enclose") {
maze.enclose();
} else {
dbc::sentinel(fmt::format("Invalid maze action {}", aname));
}
}
bool valid = maze.repair();
return {maze, valid};
}
}

54
src/algos/maze.hpp Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include "algos/matrix.hpp"
#include "game/map.hpp"
#include "algos/pathing.hpp"
#include <functional>
namespace maze {
struct Builder {
size_t $width = 0;
size_t $height = 0;
Matrix& $walls;
std::vector<Room>& $rooms;
std::unordered_map<Point, bool>& $doors;
std::vector<Point>& $dead_ends;
std::unordered_map<Point, bool> $ends_map;
Room $no_rooms_region{0,0,0,0};
// BUG: instead of bool map it to the room?
Pathing $pathing;
Builder(Map& map) :
$width(map.$width), $height(map.$height), $walls(map.$walls),
$rooms(map.$rooms), $doors(map.$doors), $dead_ends(map.$dead_ends), $pathing{$width, $height}
{
dbc::check($width % 2 == 1, "map width not an ODD number (perimter dead ends bug)");
dbc::check($height % 2 == 1, "map height not an ODD number (perimter dead ends bug)");
clear();
}
void clear();
void hunt_and_kill(Point on={1,1});
void place_rooms();
void enclose();
void randomize_rooms(size_t room_size);
void inner_donut(float outer_rad, float inner_rad);
void inner_box(size_t outer_size, size_t inner_size);
void divide(Point start, Point end);
void remove_dead_ends();
void dump(const std::string& msg, bool path_too=false);
void open_box(size_t outer_size);
void add_dead_end(Point at);
bool room_should_exist(Room& room, bool allow_dupes=false);
void place_doors();
bool validate();
bool repair();
void punch_dead_end(size_t at_x, size_t at_y, size_t x, size_t y);
bool space_available(size_t x, size_t y);
int compute_paths(size_t x, size_t y);
bool valid_door(size_t x, size_t y);
};
std::pair<Builder, bool> script(Map& map, nlohmann::json& config);
}

146
src/algos/pathing.cpp Normal file
View file

@ -0,0 +1,146 @@
#include "constants.hpp"
#include "algos/pathing.hpp"
#include "dbc.hpp"
#include <vector>
using std::vector;
inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) {
for(matrix::box it{closed, x, y, 1}; it.next();) {
if(closed[it.y][it.x] == 0) {
closed[it.y][it.x] = 1;
neighbors.emplace_back(it.x, it.y);
}
}
}
int Pathing::compute_paths(Matrix &walls) {
INVARIANT();
dbc::check(walls[0].size() == $width,
$F("Pathing::compute_paths called with walls.width={} but paths $width={}", walls[0].size(), $width));
dbc::check(walls.size() == $height,
$F("Pathing::compute_paths called with walls.height={} but paths $height={}", walls[0].size(), $height));
// Initialize the new array with every cell at limit distance
matrix::assign($paths, WALL_PATH_LIMIT);
Matrix closed = walls;
PointList starting_cells;
PointList open_cells;
// First pass: Add starting cells and put them in closed
for(size_t counter = 0; counter < $height * $width; counter++) {
size_t x = counter % $width;
size_t y = counter / $width;
if($input[y][x] == 0) {
$paths[y][x] = 0;
closed[y][x] = 1;
starting_cells.emplace_back(x,y);
}
}
// Second pass: Add border to open
for(auto sp : starting_cells) {
add_neighbors(open_cells, closed, sp.y, sp.x);
}
// Third pass: Iterate filling in the open list
int counter = 1; // leave this here so it's available below
for(; counter < WALL_PATH_LIMIT && !open_cells.empty(); ++counter) {
PointList next_open;
for(auto sp : open_cells) {
$paths[sp.y][sp.x] = counter;
add_neighbors(next_open, closed, sp.y, sp.x);
}
open_cells = next_open;
}
// Last pass: flood last cells
for(auto sp : open_cells) {
$paths[sp.y][sp.x] = counter;
}
return counter;
}
void Pathing::set_target(const Point &at, int value) {
$input[at.y][at.x] = value;
}
void Pathing::clear_target(const Point &at) {
$input[at.y][at.x] = 1;
}
PathingResult Pathing::find_path(Point &out, int direction, bool diag)
{
// get the current dijkstra number
int cur = $paths[out.y][out.x];
int target = cur;
// BUG: can I shortcut if target == 0 now?
bool found = false;
// a lambda makes it easy to capture what we have to change
auto next_step = [&](size_t x, size_t y) -> bool {
target = $paths[y][x];
// don't go through walls
// BUG: should actually do a collision check
// BUG: can I shortcut if target == 0 now?
if(target == WALL_PATH_LIMIT) return false;
int weight = cur - target;
if(weight == direction) {
out = {x, y};
found = true;
// only break if this is a lower path
return true;
} else if(weight == 0) {
out = {x, y};
found = true;
// only found an equal path, keep checking
}
// this says keep going
return false;
};
if(diag) {
// BUG: maybe a special alternative to box that doesn't do the central cell?
for(matrix::box it{$paths, out.x, out.y, 1}; it.next();) {
bool should_stop = next_step(it.x, it.y);
if(should_stop) break;
}
} else {
for(matrix::compass it{$paths, out.x, out.y}; it.next();) {
bool should_stop = next_step(it.x, it.y);
if(should_stop) break;
}
}
if(target == 0) {
return PathingResult::FOUND;
} else if(!found) {
return PathingResult::FAIL;
} else {
return PathingResult::CONTINUE;
}
}
bool Pathing::INVARIANT() {
using dbc::check;
check($paths.size() == $height, "paths wrong height");
check($paths[0].size() == $width, "paths wrong width");
check($input.size() == $height, "input wrong height");
check($input[0].size() == $width, "input wrong width");
return true;
}
void Pathing::dump(const std::string& msg) {
matrix::dump(msg, $paths);
}

41
src/algos/pathing.hpp Normal file
View file

@ -0,0 +1,41 @@
#pragma once
#include "algos/point.hpp"
#include "algos/matrix.hpp"
#include <functional>
using matrix::Matrix;
constexpr const int PATHING_TOWARD=1;
constexpr const int PATHING_AWAY=-1;
enum class PathingResult {
FAIL=0,
FOUND=1,
CONTINUE=2
};
class Pathing {
public:
size_t $width;
size_t $height;
Matrix $paths;
Matrix $input;
Pathing(size_t width, size_t height) :
$width(width),
$height(height),
$paths(height, matrix::Row(width, 1)),
$input(height, matrix::Row(width, 1))
{}
int compute_paths(Matrix &walls);
void set_target(const Point &at, int value=0);
void clear_target(const Point &at);
Matrix &paths() { return $paths; }
Matrix &input() { return $input; }
int distance(Point to) { return $paths[to.y][to.x];}
PathingResult find_path(Point &out, int direction, bool diag);
bool INVARIANT();
void dump(const std::string& msg);
};

20
src/algos/point.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <vector>
struct Point {
size_t x = 0;
size_t y = 0;
bool operator==(const Point& other) const {
return other.x == x && other.y == y;
}
};
typedef std::vector<Point> PointList;
template<> struct std::hash<Point> {
size_t operator()(const Point& p) const {
auto hasher = std::hash<int>();
return hasher(p.x) ^ hasher(p.y);
}
};

12
src/algos/rand.cpp Normal file
View file

@ -0,0 +1,12 @@
#include "algos/rand.hpp"
namespace Random {
std::random_device RNG;
std::mt19937 GENERATOR(RNG());
std::chrono::milliseconds milliseconds(int from, int to) {
int tick = Random::uniform_real(float(from), float(to));
return std::chrono::milliseconds{tick};
}
}

41
src/algos/rand.hpp Normal file
View file

@ -0,0 +1,41 @@
#pragma once
#include <random>
#include <chrono>
namespace Random {
extern std::mt19937 GENERATOR;
template<typename T>
T uniform(T from, T to) {
std::uniform_int_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T uniform_real(T from, T to) {
std::uniform_real_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T normal(T mean, T stddev) {
std::normal_distribution<T> rand(mean, stddev);
return rand(GENERATOR);
}
auto abs(auto i, auto j) {
if(i < j) {
return Random::uniform(i, j);
} else if(j < i) {
return Random::uniform(j, i);
} else {
return i;
}
}
std::chrono::milliseconds milliseconds(int from, int to);
}

674
src/algos/shiterator.hpp Normal file
View file

@ -0,0 +1,674 @@
#pragma once
#include <vector>
#include <queue>
#include <string>
#include <array>
#include <numeric>
#include <algorithm>
#include <fmt/core.h>
#include "algos/point.hpp"
#include "algos/rand.hpp"
#include "dbc.hpp"
/*
* # What is This Shit?
*
* Announcing the Shape Iterators, or "shiterators" for short. The best shite
* for C++ for-loops since that [one youtube
* video](https://www.youtube.com/watch?v=rX0ItVEVjHc) told everyone to
* recreate SQL databases with structs. You could also say these are Shaw's
* Iterators, but either way they are the _shite_. Or are they shit? You decide.
* Maybe they're "shite"?
*
* A shiterator is a simple generator that converts 2D shapes into a 1D stream
* of x/y coordinates. You give it a matrix, some parameters like start, end,
* etc. and each time you call `next()` you get the next viable x/y coordinate to
* complete the shape. This makes them far superior to _any_ existing for-loop
* technology because shiterators operate _intelligently_ in shapes. Other
* [programming pundits](https://www.youtube.com/watch?v=tD5NrevFtbU) will say
* their 7000 line "easy to maintain" switch statements are better at drawing
* shapes, but they're wrong. My way of making a for-loop do stuff is vastly
* superior because it doesn't use a switch _or_ a virtual function _or_
* inheritance at all. That means they have to be the _fastest_. Feel free to run
* them 1000 times and bask in the glory of 1 nanosecond difference performance.
*
* It's science and shite.
*
* More importantly, shiterators are simple and easy to use. They're so easy to
* use you _don't even use the 3rd part of the for-loop_. What? You read that right,
* not only have I managed to eliminate _both_ massive horrible to maintain switches,
* and also avoided virtual functions, but I've also _eliminated one entire part
* of the for-loop_. This obviously makes them way faster than other inferior
* three-clause-loop-trash. Just look at this comparison:
*
* ```cpp
* for(it = trash.begin(); it != trash.end(); it++) {
* std::cout << it << std::endl;
* }
* ```
*
* ```cpp
* for(each_cell it{mat}; it.next();) {
* std::cout << mat[it.y][it.x] << std::endl;
* }
* ```
*
* Obviously this will outperform _any_ iterator invented in the last 30 years, but the best
* thing about shiterators is their composability and ability to work simultaneously across
* multiple matrices in one loop:
*
* ```cpp
* for(line it{start, end}; it.next();) {
* for(compass neighbor{walls, it.x, it.y}; neighbor.next();) {
* if(walls[neighbor.y][neighbor.x] == 1) {
* wall_update[it.y][it.x] = walls[it.y][it.x] + 10;
* }
* }
* }
* ```
*
* This code sample (maybe, because I didn't run it) draws a line from
* `start` to `end` then looks at each neighbor on a compass (north, south, east, west)
* at each point to see if it's set to 1. If it is then it copies that cell over to
* another matrix with +10. Why would you need this? Your Wizard just shot a fireball
* down a corridor and you need to see if anything in the path is within 1 square of it.
*
* You _also_ don't even need to use a for-loop. Yes, you can harken back to the old
* days when we did everything RAW inside a Duff's Device between a while-loop for
* that PERFORMANCE because who cares about maintenance? You're a game developer! Tests?
* Don't need a test if it runs fine on Sony Playstation only. Maintenance? You're moving
* on to the next project in two weeks anyway right?! Use that while-loop and a shiterator
* to really help that next guy:
*
* ```cpp
* box it{walls, center_x, center_y, 20};
* while(it.next()) {
* walls[it.y][it.x] = 1;
* }
* ```
*
* ## Shiterator "Guarantees"
*
* Just like Rust [guarantees no memory leaks](https://github.com/pop-os/cosmic-comp/issues/1133),
* a shiterator tries to ensure a few things, if it can:
*
* 1. All x/y values will be within the Matrix you give it. The `line` shiterator doesn't though.
* 2. They try to not store anything and only calculate the math necessary to linearlize the shape.
* 3. You can store them and incrementally call next to get the next value.
* 4. You should be able to compose them together on the same Matrix or different matrices of the same dimensions.
* 5. Most of them will only require 1 for-loop, the few that require 2 only do this so you can draw the inside of a shape. `circle` is like this.
* 6. They don't assume any particular classes or require subclassing. As long as the type given enables `mat[y][x]` (row major) access then it'll work.
* 7. The matrix given to a shiterator isn't actually attached to it, so you can use one matrix to setup an iterator, then apply the x/y values to any other matrix of the same dimensions. Great for smart copying and transforming.
* 8. More importantly, shiterators _do not return any values from the matrix_. They only do the math for coordinates and leave it to you to work your matrix.
*
* These shiterators are used all over the game to do map rendering, randomization, drawing, nearly everything that involves a shape.
*
* ## Algorithms I Need
*
* I'm currently looking for a few algorithms, so if you know how to do these let me know:
*
* 1. _Flood fill_ This turns out to be really hard because most algorithms require keeping track of visited cells with a queue, recursion, etc.
* 2. _Random rectangle fill_ I have something that mostly works but it's really only random across each y-axis, then separate y-axes are randomized.
* 3. _Dijkstra Map_ I have a Dijkstra algorithm but it's not in this style yet. Look in `worldbuilder.cpp` for my current implementation.
* 4. _Viewport_ Currently working on this but I need to have a rectangle I can move around as a viewport.
*
*
* ## Usage
*
* Check the `algos/matrix.hpp` for an example if you want to make it more conventient for your own type.
*
* ## Thanks
*
* Special thanks to Amit and hirdrac for their help with the math and for
* giving me the initial idea. hirdrac doesn't want to be held responsible for
* this travesty but he showed me that you can do iteration and _not_ use the
* weird C++ iterators. Amit did a lot to show me how to do these calculations
* without branching. Thanks to you both--and to everyone else--for helping me while I
* stream my development.
*
* ### SERIOUS DISCLAIMER
*
* I am horribly bad at trigonometry and graphics algorithms, so if you've got an idea to improve them
* or find a bug shoot me an email at help@learncodethehardway.com.
*/
namespace shiterator {
using std::vector, std::queue, std::array;
using std::min, std::max, std::floor;
template<typename T>
using BaseRow = vector<T>;
template<typename T>
using Base = vector<BaseRow<T>>;
template<typename T>
inline Base<T> make(size_t width, size_t height) {
Base<T> result(height, BaseRow<T>(width));
return result;
}
/*
* Just a quick thing to reset a matrix to a value.
*/
template<typename MAT, typename VAL>
inline void assign(MAT &out, VAL new_value) {
for(auto &row : out) {
row.assign(row.size(), new_value);
}
}
/*
* Tells you if a coordinate is in bounds of the matrix
* and therefore safe to use.
*/
template<typename MAT>
inline bool inbounds(MAT &mat, size_t x, size_t y) {
// since Point.x and Point.y are size_t any negatives are massive
return (y < mat.size()) && (x < mat[0].size());
}
/*
* Gives the width of a matrix. Assumes row major (y/x)
* and vector API .size().
*/
template<typename MAT>
inline size_t width(MAT &mat) {
return mat[0].size();
}
/*
* Same as shiterator::width but just the height.
*/
template<typename MAT>
inline size_t height(MAT &mat) {
return mat.size();
}
/*
* These are internal calculations that help
* with keeping track of the next x coordinate.
*/
inline size_t next_x(size_t x, size_t width) {
return (x + 1) * ((x + 1) < width);
}
/*
* Same as next_x but updates the next y coordinate.
* It uses the fact that when x==0 you have a new
* line so increment y.
*/
inline size_t next_y(size_t x, size_t y) {
return y + (x == 0);
}
/*
* Figures out if you're at the end of the shape,
* which is usually when y > height.
*/
inline bool at_end(size_t y, size_t height) {
return y < height;
}
/*
* Determines if you're at the end of a row.
*/
inline bool end_row(size_t x, size_t width) {
return x == width - 1;
}
/*
* Most basic shiterator. It just goes through
* every cell in the matrix in linear order
* with not tracking of anything else.
*/
template<typename MAT>
struct each_cell_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
each_cell_t(MAT &mat)
{
height = shiterator::height(mat);
width = shiterator::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
return at_end(y, height);
}
};
/*
* This is just each_cell_t but it sets
* a boolean value `bool row` so you can
* tell when you've reached the end of a
* row. This is mostly used for printing
* out a matrix and similar just drawing the
* whole thing with its boundaries.
*/
template<typename MAT>
struct each_row_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
bool row = false;
each_row_t(MAT &mat) {
height = shiterator::height(mat);
width = shiterator::width(mat);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
row = end_row(x, width);
return at_end(y, height);
}
};
/*
* This is a CENTERED box, that will create
* a centered rectangle around a point of a
* certain dimension. This kind of needs a
* rewrite but if you want a rectangle from
* a upper corner then use rectangle_t type.
*
* Passing 1 parameter for the size will make
* a square.
*/
template<typename MAT>
struct box_t {
size_t from_x;
size_t from_y;
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
size_t left = 0;
size_t top = 0;
size_t right = 0;
size_t bottom = 0;
box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) :
box_t(mat, at_x, at_y, size, size) {
}
box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) :
from_x(at_x), from_y(at_y)
{
size_t h = shiterator::height(mat);
size_t w = shiterator::width(mat);
// keeps it from going below zero
// need extra -1 to compensate for the first next()
left = max(from_x, width) - width;
x = left - 1; // must be -1 for next()
// keeps it from going above width
right = min(from_x + width + 1, w);
// same for these two
top = max(from_y, height) - height;
y = top - (left == 0);
bottom = min(from_y + height + 1, h);
}
bool next() {
// calc next but allow to go to 0 for next
x = next_x(x, right);
// x will go to 0, which signals new line
y = next_y(x, y); // this must go here
// if x==0 then this moves it to min_x
x = max(x, left);
// and done
return at_end(y, bottom);
}
/*
* This was useful for doing quick lighting
* calculations, and I might need to implement
* it in other shiterators. It gives the distance
* to the center from the current x/y.
*/
float distance() {
int dx = from_x - x;
int dy = from_y - y;
return sqrt((dx * dx) + (dy * dy));
}
};
/*
* Stupid simple compass shape North/South/East/West.
* This comes up a _ton_ when doing searching, flood
* algorithms, collision, etc. Probably not the
* fastest way to do it but good enough.
*/
template<typename MAT>
struct compass_t {
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
array<int, 4> x_dirs{0, 1, 0, -1};
array<int, 4> y_dirs{-1, 0, 1, 0};
size_t max_dirs=0;
size_t dir = ~0;
compass_t(MAT &mat, size_t x, size_t y) :
x(x), y(y)
{
array<int, 4> x_in{0, 1, 0, -1};
array<int, 4> y_in{-1, 0, 1, 0};
for(size_t i = 0; i < 4; i++) {
int nx = x + x_in[i];
int ny = y + y_in[i];
if(shiterator::inbounds(mat, nx, ny)) {
x_dirs[max_dirs] = nx;
y_dirs[max_dirs] = ny;
max_dirs++;
}
}
}
bool next() {
dir++;
if(dir < max_dirs) {
x = x_dirs[dir];
y = y_dirs[dir];
return true;
} else {
return false;
}
}
};
/*
* Draws a line from start to end using a algorithm from
* https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
* No idea if the one I picked is best but it's the one
* that works in the shiterator requirements and produced
* good results.
*
* _WARNING_: This one doesn't check if the start/end are
* within your Matrix, as it's assumed _you_ did that
* already.
*/
struct line_t {
int x;
int y;
int x1;
int y1;
int sx;
int sy;
int dx;
int dy;
int error;
line_t(Point start, Point end) :
x(start.x), y(start.y),
x1(end.x), y1(end.y)
{
dx = std::abs(x1 - x);
sx = x < x1 ? 1 : -1;
dy = std::abs(y1 - y) * -1;
sy = y < y1 ? 1 : -1;
error = dx + dy;
}
bool next() {
if(x != x1 || y != y1) {
int e2 = 2 * error;
if(e2 >= dy) {
error = error + dy;
x = x + sx;
}
if(e2 <= dx) {
error = error + dx;
y = y + sy;
}
return true;
} else {
return false;
}
}
};
/*
* Draws a simple circle using a fairly naive algorithm
* but one that actually worked. So, so, so, so many
* circle drawing algorithms described online don't work
* or are flat wrong. Even the very best I could find
* did overdrawing of multiple lines or simply got the
* math wrong. Keep in mind, _I_ am bad at this trig math
* so if I'm finding errors in your circle drawing then
* you got problems.
*
* This one is real simple, and works. If you got better
* then take the challenge but be ready to get it wrong.
*/
template<typename MAT>
struct circle_t {
float center_x;
float center_y;
float radius = 0.0f;
int y = 0;
int dx = 0;
int dy = 0;
int left = 0;
int right = 0;
int top = 0;
int bottom = 0;
int width = 0;
int height = 0;
circle_t(MAT &mat, Point center, float radius) :
center_x(center.x), center_y(center.y), radius(radius)
{
width = shiterator::width(mat);
height = shiterator::height(mat);
top = max(int(floor(center_y - radius)), 0);
bottom = min(int(floor(center_y + radius)), height - 1);
y = top;
}
bool next() {
y++;
if(y <= bottom) {
dy = y - center_y;
dx = floor(sqrt(radius * radius - dy * dy));
left = max(0, int(center_x) - dx);
right = min(width, int(center_x) + dx + 1);
return true;
} else {
return false;
}
}
};
/*
* Basic rectangle shiterator, and like box and rando_rect_t you can
* pass only 1 parameter for size to do a square.
*/
template<typename MAT>
struct rectangle_t {
int x;
int y;
int top;
int left;
int width;
int height;
int right;
int bottom;
rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
rectangle_t(mat, start_x, start_y, size, size) {
}
rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) :
top(start_y),
left(start_x),
width(width),
height(height)
{
size_t h = shiterator::height(mat);
size_t w = shiterator::width(mat);
y = start_y - 1;
x = left - 1; // must be -1 for next()
right = min(start_x + width, w);
y = start_y;
bottom = min(start_y + height, h);
}
bool next() {
x = next_x(x, right);
y = next_y(x, y);
x = max(x, left);
return at_end(y, bottom);
}
};
/*
* Same as rando_rect_t but it uses a centered box.
*/
template<typename MAT>
struct rando_box_t {
size_t x;
size_t y;
size_t x_offset;
size_t y_offset;
box_t<MAT> it;
rando_box_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
it{mat, start_x, start_y, size}
{
x_offset = Random::uniform(size_t(0), it.right);
y_offset = Random::uniform(size_t(0), it.bottom);
}
bool next() {
bool done = it.next();
x = it.left + ((it.x + x_offset) % it.right);
y = it.top + ((it.y + y_offset) % it.bottom);
return done;
}
};
/*
* WIP: This one is used to place entities randomly but
* could be used for effects like random destruction of floors.
* It simply "wraps" the rectangle_t but randomizes the x/y values
* using a random starting point. This makes it random across the
* x-axis but only partially random across the y.
*/
template<typename MAT>
struct rando_rect_t {
int x;
int y;
int x_offset;
int y_offset;
rectangle_t<MAT> it;
rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
rando_rect_t(mat, start_x, start_y, size, size) {
}
rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) :
it{mat, start_x, start_y, width, height}
{
x_offset = Random::uniform(0, int(width));
y_offset = Random::uniform(0, int(height));
}
bool next() {
bool done = it.next();
x = it.left + ((it.x + x_offset) % it.width);
y = it.top + ((it.y + y_offset) % it.height);
return done;
}
};
template<typename MAT>
struct perimeter_t {
size_t x;
size_t y;
size_t width;
size_t height;
size_t i = 0;
std::array<Point, 4> starts{{
{x,y}, {x + width-1, y}, {x + width-1, y + height-1}, {x, y + height-1}
}};
std::array<Point, 4> ends{{
{x + width-1, y}, {x + width-1, y + height-1}, {x, y + height-1}, {x,y},
}};
line_t it{starts[i], ends[i]};
bool next() {
if(i >= starts.size()) return false;
if(it.next()) {
x = it.x;
y = it.y;
return true;
} else {
x = 0;
y = 0;
i++;
if(i < starts.size()) {
it = {starts[i], ends[i]};
return true;
} else {
return false;
}
}
}
};
/*
* BROKEN: I'm actually not sure what I'm trying to
* do here yet.
*/
template<typename MAT>
struct viewport_t {
Point start;
// this is the point in the map
size_t x;
size_t y;
// this is the point inside the box, start at 0
size_t view_x = ~0;
size_t view_y = ~0;
// viewport width/height
size_t width;
size_t height;
viewport_t(MAT &mat, Point start, int max_x, int max_y) :
start(start),
x(start.x-1),
y(start.y-1)
{
width = std::min(size_t(max_x), shiterator::width(mat) - start.x);
height = std::min(size_t(max_y), shiterator::height(mat) - start.y);
fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}",
max_x, max_y, shiterator::width(mat), shiterator::height(mat), x, y);
}
bool next() {
y = next_y(x, y);
x = next_x(x, width);
view_x = next_x(view_x, width);
view_y = next_y(view_x, view_y);
return at_end(y, height);
}
};
}

32
src/algos/simplefsm.hpp Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include <fmt/core.h>
#ifndef FSM_DEBUG
#define FSM_STATE(C, S, E, ...) case C::S: S(E, ##__VA_ARGS__); break
#else
static int last_event=-1;
#define FSM_STATE(C, S, E, ...) case C::S: if(last_event != int(E)) { last_event = int(E); fmt::println(">> " #C " " #S " event={}, state={}", int(E), int($state));}; S(E, ##__VA_ARGS__); break
#endif
template<typename S, typename E>
class DeadSimpleFSM {
protected:
// BUG: don't put this in your class because state() won't work
S $state = S::START;
public:
template<typename... Types>
void event(E event, Types... args);
void state(S next_state) {
#ifdef FSM_DEBUG
fmt::println("<< STATE: {} -> {}", int($state), int(next_state));
#endif
$state = next_state;
}
bool in_state(S state) {
return $state == state;
}
};

138
src/algos/spatialmap.cpp Normal file
View file

@ -0,0 +1,138 @@
#include "algos/spatialmap.hpp"
#include <fmt/core.h>
using namespace fmt;
using DinkyECS::Entity;
void SpatialMap::insert(Point pos, Entity ent, bool has_collision) {
if(has_collision) {
dbc::check(!occupied(pos), "attempt to insert an entity with collision in space with collision");
}
$collision.emplace(pos, CollisionData{ent, has_collision});
}
CollisionData SpatialMap::remove(Point pos, Entity ent) {
auto [begin, end] = $collision.equal_range(pos);
for(auto it = begin; it != end; ++it) {
if(it->second.entity == ent) {
// does the it->second go invalid after erase?
auto copy = it->second;
$collision.erase(it);
return copy;
}
}
dbc::sentinel("failed to find entity to remove");
}
void SpatialMap::move(Point from, Point to, Entity ent) {
auto data = remove(from, ent);
insert(to, ent, data.collision);
}
Entity SpatialMap::occupied_by(Point at) const {
auto [begin, end] = $collision.equal_range(at);
for(auto it = begin; it != end; ++it) {
if(it->second.collision) {
return it->second.entity;
}
}
return DinkyECS::NONE;
}
bool SpatialMap::occupied(Point at) const {
return occupied_by(at) != DinkyECS::NONE;
}
bool SpatialMap::something_there(Point at) const {
return $collision.count(at) > 0;
}
Entity SpatialMap::get(Point at) const {
dbc::check($collision.contains(at), "attempt to get entity when none there");
auto [begin, end] = $collision.equal_range(at);
return begin->second.entity;
}
void SpatialMap::find_neighbor(EntityList &result, Point at, int dy, int dx) const {
// don't bother checking for cells out of bounds
if((dx < 0 && at.x <= 0) || (dy < 0 && at.y <= 0)) {
return;
}
Point cell = {at.x + dx, at.y + dy};
auto entity = find(cell, [&](auto data) {
return data.collision;
});
if(entity != DinkyECS::NONE) result.push_back(entity);
}
FoundEntities SpatialMap::neighbors(Point cell, bool diag) const {
EntityList result;
// just unroll the loop since we only check four directions
// this also solves the problem that it was detecting that the cell was automatically included as a "neighbor" but it's not
find_neighbor(result, cell, 0, 1); // north
find_neighbor(result, cell, 0, -1); // south
find_neighbor(result, cell, 1, 0); // east
find_neighbor(result, cell, -1, 0); // west
if(diag) {
find_neighbor(result, cell, 1, -1); // south east
find_neighbor(result, cell, -1, -1); // south west
find_neighbor(result, cell, 1, 1); // north east
find_neighbor(result, cell, -1, 1); // north west
}
return {!result.empty(), result};
}
inline void update_sorted(SortedEntities& sprite_distance, PointEntityMap& table, Point from, int max_dist) {
Point seen{0,0};
float wiggle = 0.0f;
for(const auto &rec : table) {
Point sprite = rec.first;
int inside = (from.x - sprite.x) * (from.x - sprite.x) +
(from.y - sprite.y) * (from.y - sprite.y);
if(from == sprite || rec.second.collision) {
wiggle = 0.0f;
} else if(sprite == seen) {
wiggle += 0.02f;
} else {
wiggle = 0.0f;
seen = sprite;
}
if(inside < max_dist) {
sprite_distance.push_back({inside, rec.second.entity, wiggle});
}
}
}
Entity SpatialMap::find(Point at, std::function<bool(CollisionData)> cb) const {
auto [begin, end] = $collision.equal_range(at);
for(auto it = begin; it != end; ++it) {
if(cb(it->second)) return it->second.entity;
}
return DinkyECS::NONE;
}
void SpatialMap::distance_sorted(SortedEntities& sprite_distance, Point from, int max_dist) {
sprite_distance.clear();
update_sorted(sprite_distance, $collision, from, max_dist);
std::sort(sprite_distance.begin(), sprite_distance.end(), [](auto &a, auto &b) {
return a.dist_square > b.dist_square;
});
}

49
src/algos/spatialmap.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include <vector>
#include <unordered_map>
#include "game/map.hpp"
#include "algos/dinkyecs.hpp"
#include "algos/point.hpp"
struct CollisionData {
DinkyECS::Entity entity = DinkyECS::NONE;
bool collision = false;
};
struct EntityDistance {
int dist_square=0;
DinkyECS::Entity entity=DinkyECS::NONE;
float wiggle=0.0f;
};
// Point's has is in point.hpp
using EntityList = std::vector<DinkyECS::Entity>;
using PointEntityMap = std::unordered_multimap<Point, CollisionData>;
using SortedEntities = std::vector<EntityDistance>;
struct FoundEntities {
bool found;
EntityList nearby;
};
class SpatialMap {
public:
SpatialMap() {}
PointEntityMap $collision;
void insert(Point pos, DinkyECS::Entity obj, bool has_collision);
void move(Point from, Point to, DinkyECS::Entity ent);
// return value is whether the removed thing has collision
CollisionData remove(Point pos, DinkyECS::Entity entity);
DinkyECS::Entity occupied_by(Point pos) const;
bool occupied(Point pos) const;
bool something_there(Point at) const;
DinkyECS::Entity get(Point at) const;
DinkyECS::Entity find(Point at, std::function<bool(CollisionData)> cb) const;
void find_neighbor(EntityList &result, Point at, int dy, int dx) const;
FoundEntities neighbors(Point position, bool diag=false) const;
void distance_sorted(SortedEntities& sorted_sprites, Point from, int max_distance);
size_t size() { return $collision.size(); }
};

11
src/algos/stats.cpp Normal file
View file

@ -0,0 +1,11 @@
#include "algos/stats.hpp"
#include <fmt/core.h>
#include "dbc.hpp"
void Stats::dump(std::string msg)
{
dbc::log($F("{}: sum: {}, sumsq: {}, n: {}, "
"min: {}, max: {}, mean: {}, stddev: {}",
msg, sum, sumsq, n, min, max, mean(),
stddev()));
}

59
src/algos/stats.hpp Normal file
View file

@ -0,0 +1,59 @@
#pragma once
#include <cmath>
#include <chrono>
struct Stats {
using TimeBullshit = std::chrono::time_point<std::chrono::high_resolution_clock>;
double sum = 0.0;
double sumsq = 0.0;
double n = 0.0;
double min = 0.0;
double max = 0.0;
inline void reset() {
sum = 0.0;
sumsq = 0.0;
n = 0.0;
min = 0.0;
max = 0.0;
}
inline double mean() {
return sum / n;
}
inline double stddev() {
return std::sqrt((sumsq - (sum * sum / n)) / (n - 1));
}
inline void sample(double s) {
sum += s;
sumsq += s * s;
if (n == 0) {
min = s;
max = s;
} else {
if (min > s) min = s;
if (max < s) max = s;
}
n += 1;
}
inline TimeBullshit time_start() {
return std::chrono::high_resolution_clock::now();
}
inline void sample_time(TimeBullshit start) {
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration<double>(end - start);
if(elapsed.count() > 0.0) {
sample(1.0/elapsed.count());
}
}
void dump(std::string msg="");
};

127
src/combat/battle.cpp Normal file
View file

@ -0,0 +1,127 @@
#include "combat/battle.hpp"
namespace combat {
void BattleEngine::add_enemy(Combatant enemy) {
$combatants.try_emplace(enemy.entity, enemy);
if(enemy.is_host) {
dbc::check($host_combat == nullptr, "added the host twice!");
$host_combat = enemy.combat;
}
}
bool BattleEngine::player_request(const std::string& request) {
auto action = ai::load_action(request);
bool can_go = player_pending_ap() >= action.cost;
if(can_go) {
$player_requests.try_emplace(request, action);
}
return can_go;
}
void BattleEngine::clear_requests() {
$player_requests.clear();
}
void BattleEngine::ap_refresh() {
for(auto& [entity, enemy] : $combatants) {
if(enemy.combat->ap < enemy.combat->max_ap) {
int new_ap = std::min(enemy.combat->max_ap, enemy.combat->ap_delta + enemy.combat->ap);
enemy.combat->ap = new_ap;
}
}
}
int BattleEngine::player_pending_ap() {
dbc::check($host_combat != nullptr, "didn't set host before checking AP");
int pending_ap = $host_combat->ap;
for(auto& [name, action] : $player_requests) {
pending_ap -= action.cost;
}
return pending_ap;
}
bool BattleEngine::plan() {
using enum BattleHostState;
int active = 0;
bool had_host = false;
for(auto& [entity, enemy] : $combatants) {
//NOTE: this is just for asserting I'm using things right
if(enemy.is_host) had_host = true;
enemy.ai->update();
active += enemy.ai->active();
if(enemy.ai->active()) {
for(auto& action : enemy.ai->plan.script) {
BattleHostState host_state = not_host;
if(action.cost > enemy.combat->ap) {
host_state = out_of_ap;
} else if(enemy.is_host) {
host_state = $player_requests.contains(action.name) ? agree : disagree;
}
if(host_state != out_of_ap) {
enemy.combat->ap -= action.cost;
}
$pending_actions.emplace_back(enemy, action.name, action.cost, host_state);
}
dbc::check(enemy.combat->ap >= 0, "enemy's AP went below 0");
dbc::check(enemy.combat->ap <= enemy.combat->max_ap, "enemy's AP went above max");
}
}
dbc::check(had_host, "FAIL, you forgot to set enemy.is_host=true for one entity");
if($pending_actions.size() > 0) {
std::sort($pending_actions.begin(), $pending_actions.end(),
[](const auto& a, const auto& b) -> bool
{
return a.cost > b.cost;
});
}
return active > 0;
}
std::optional<BattleResult> BattleEngine::next() {
if($pending_actions.size() == 0) return std::nullopt;
auto ba = $pending_actions.back();
$pending_actions.pop_back();
return std::make_optional(ba);
}
void BattleEngine::dump() {
for(auto& [entity, enemy] : $combatants) {
fmt::println("\n\n###### ENTITY #{}", entity);
enemy.ai->dump();
}
}
void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) {
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& action = $combatants.at(entity);
action.ai->set_state(state, setting);
}
void BattleEngine::set_all(const std::string& state, bool setting) {
for(auto& [ent, action] : $combatants) {
action.ai->set_state(state, setting);
}
}
Combatant& BattleEngine::get_enemy(DinkyECS::Entity entity) {
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
return $combatants.at(entity);
}
}

50
src/combat/battle.hpp Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include "game/config.hpp"
#include "algos/dinkyecs.hpp"
#include <optional>
#include "game/components.hpp"
#include <unordered_map>
#include "ai/ai.hpp"
namespace combat {
enum class BattleHostState {
not_host = 0,
agree = 1,
disagree = 2,
out_of_ap = 3
};
struct Combatant {
DinkyECS::Entity entity = DinkyECS::NONE;
ai::EntityAI* ai = nullptr;
components::Combat* combat = nullptr;
bool is_host=false;
};
struct BattleResult {
Combatant enemy;
std::string wants_to;
int cost;
BattleHostState host_state;
};
struct BattleEngine {
std::unordered_map<DinkyECS::Entity, Combatant> $combatants;
std::vector<BattleResult> $pending_actions;
std::unordered_map<std::string, ai::Action> $player_requests;
components::Combat* $host_combat = nullptr;
void add_enemy(Combatant ba);
Combatant& get_enemy(DinkyECS::Entity entity);
bool plan();
std::optional<BattleResult> next();
void dump();
void set(DinkyECS::Entity entity, const std::string& state, bool setting);
void set_all(const std::string& state, bool setting);
bool player_request(const std::string& request);
int player_pending_ap();
void clear_requests();
void ap_refresh();
};
}

16
src/combat/combat.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "game/components.hpp"
#include "algos/rand.hpp"
namespace components {
int Combat::attack(Combat &target) {
int attack = Random::uniform<int>(0,1);
int my_dmg = 0;
if(attack) {
my_dmg = Random::uniform<int>(1, damage);
target.hp -= my_dmg;
}
return my_dmg;
}
}

82
src/constants.hpp Normal file
View file

@ -0,0 +1,82 @@
#pragma once
#include <string>
#include <array>
constexpr int INV_SLOTS=16;
constexpr int TEXTURE_WIDTH=256;
constexpr int TEXTURE_HEIGHT=256;
constexpr int RAY_VIEW_WIDTH=1080;
constexpr int RAY_VIEW_HEIGHT=720;
constexpr int SCREEN_WIDTH=1280;
constexpr int SCREEN_HEIGHT=720;
constexpr int RAY_VIEW_X=SCREEN_WIDTH - RAY_VIEW_WIDTH;
constexpr int RAY_VIEW_Y=0;
constexpr int GLOW_LIMIT=220;
constexpr int LIGHT_MULTIPLIER=2.5;
constexpr float AIMED_AT_BRIGHTNESS=0.2f;
constexpr int MAP_TILE_DIM=64;
constexpr int ICONGEN_MAP_TILE_DIM=64;
constexpr int PLAYER_SPRITE_DIR_CORRECTION=270;
constexpr int RENDER_DISTANCE=500;
constexpr int ROOM_SIZE=3;
constexpr int BOSS_VIEW_WIDTH=1080;
constexpr int BOSS_VIEW_HEIGHT=SCREEN_HEIGHT;
constexpr int BOSS_VIEW_X=SCREEN_WIDTH - BOSS_VIEW_WIDTH;
constexpr int BOSS_VIEW_Y=0;
constexpr bool VSYNC=true;
constexpr int FRAME_LIMIT=60;
constexpr int NUM_SPRITES=1;
constexpr int MAX_LOG_MESSAGES=17;
#ifdef NDEBUG
constexpr bool DEBUG_BUILD=false;
#else
constexpr bool DEBUG_BUILD=true;
#endif
////////// copied from roguish
constexpr int INV_WALL = 0;
constexpr int INV_SPACE = 1;
constexpr int WALL_VALUE = 1;
constexpr int SPACE_VALUE = 0;
constexpr int WALL_PATH_LIMIT = 1000;
constexpr int ROOM_SPACE_VALUE = 1001;
constexpr int DOOR_VALUE = 1002;
constexpr int DEAD_END_VALUE = 1003;
constexpr int WALL_LIGHT_LEVEL = 3;
constexpr int WORLDBUILD_DIVISION = 4;
constexpr int WORLDBUILD_SHRINK = 2;
constexpr int WORLDBUILD_MAX_PATH = 200;
constexpr int UI_FONT_SIZE=20;
constexpr int BASE_MAP_FONT_SIZE=80;
constexpr int GAME_MAP_PIXEL_POS = 600;
constexpr int MAX_FONT_SIZE = 140;
constexpr int MIN_FONT_SIZE = 20;
constexpr float PERCENT = 0.01f;
constexpr int STATUS_UI_X = 0;
constexpr int STATUS_UI_Y = 0;
constexpr int STATUS_UI_WIDTH = SCREEN_WIDTH - RAY_VIEW_WIDTH;
constexpr int STATUS_UI_HEIGHT = SCREEN_HEIGHT;
constexpr int INITIAL_MAP_W = 21;
constexpr int INITIAL_MAP_H = 21;
constexpr float DEFAULT_ROTATE=0.5f;
// for the panels/renderer
constexpr wchar_t BG_TILE = L'';
constexpr wchar_t UI_BASE_CHAR = L'';
constexpr int BG_BOX_OFFSET=5;
constexpr const char *FONT_FILE_NAME="assets/text.otf";
constexpr std::array<std::wstring, 8> COMPASS{
// L"E", L"SE", L"S", L"SW", L"W", L"NW", L"N", L"NE"
L"\u2192", L"\u2198", L"\uffec", L"\u21d9", L"\u2190", L"\u2196", L"\uffea", L"\u21d7" };

47
src/dbc.cpp Normal file
View file

@ -0,0 +1,47 @@
#include "dbc.hpp"
#include <iostream>
void dbc::log(const string &message, const std::source_location location) {
std::cout << '[' << location.file_name() << ':'
<< location.line() << "|"
<< location.function_name() << "] "
<< message << std::endl;
}
void dbc::sentinel(const string &message, const std::source_location location) {
string err = $F("[SENTINEL!] {}", message);
dbc::log(err, location);
throw dbc::SentinelError{err};
}
void dbc::pre(const string &message, bool test, const std::source_location location) {
if(!test) {
string err = $F("[PRE!] {}", message);
dbc::log(err, location);
throw dbc::PreCondError{err};
}
}
void dbc::pre(const string &message, std::function<bool()> tester, const std::source_location location) {
dbc::pre(message, tester(), location);
}
void dbc::post(const string &message, bool test, const std::source_location location) {
if(!test) {
string err = $F("[POST!] {}", message);
dbc::log(err, location);
throw dbc::PostCondError{err};
}
}
void dbc::post(const string &message, std::function<bool()> tester, const std::source_location location) {
dbc::post(message, tester(), location);
}
void dbc::check(bool test, const string &message, const std::source_location location) {
if(!test) {
string err = $F("[CHECK!] {}\n", message);
dbc::log(err, location);
throw dbc::CheckError{err};
}
}

53
src/dbc.hpp Normal file
View file

@ -0,0 +1,53 @@
#pragma once
#include <string>
#include <fmt/core.h>
#include <functional>
#include <source_location>
// AKA the Fuckit macro
#define $F(FMT, ...) fmt::format(FMT, ##__VA_ARGS__)
namespace dbc {
using std::string;
class Error {
public:
const string message;
Error(string m) : message{m} {}
Error(const char *m) : message{m} {}
};
class CheckError : public Error {};
class SentinelError : public Error {};
class PreCondError : public Error {};
class PostCondError : public Error {};
void log(const string &message,
const std::source_location location =
std::source_location::current());
[[noreturn]] void sentinel(const string &message,
const std::source_location location =
std::source_location::current());
void pre(const string &message, bool test,
const std::source_location location =
std::source_location::current());
void pre(const string &message, std::function<bool()> tester,
const std::source_location location =
std::source_location::current());
void post(const string &message, bool test,
const std::source_location location =
std::source_location::current());
void post(const string &message, std::function<bool()> tester,
const std::source_location location =
std::source_location::current());
void check(bool test, const string &message,
const std::source_location location =
std::source_location::current());
}

49
src/events.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
namespace game {
enum Event {
AIM_CLICK=__LINE__,
ANIMATION_END=__LINE__,
ANIMATION_START=__LINE__,
ATTACK=__LINE__,
BOSS_END=__LINE__,
BOSS_START=__LINE__,
CLOSE=__LINE__,
COMBAT=__LINE__,
COMBAT_START=__LINE__,
COMBAT_STOP=__LINE__,
DEATH=__LINE__,
ENTITY_SPAWN=__LINE__,
HP_STATUS=__LINE__,
INV_SELECT=__LINE__,
KEY_PRESS=__LINE__,
LOOT_CLOSE=__LINE__,
LOOT_CONTAINER=__LINE__,
LOOT_ITEM=__LINE__,
LOOT_OPEN=__LINE__,
LOOT_SELECT=__LINE__,
MAP_OPEN=__LINE__,
MOUSE_CLICK=__LINE__,
MOUSE_DRAG=__LINE__,
MOUSE_DRAG_START=__LINE__,
MOUSE_DROP=__LINE__,
MOUSE_MOVE=__LINE__,
MOVE_BACK=__LINE__,
MOVE_FORWARD=__LINE__,
MOVE_LEFT=__LINE__,
MOVE_RIGHT=__LINE__,
NEW_RITUAL=__LINE__,
NOOP=__LINE__,
NO_NEIGHBORS=__LINE__,
QUIT=__LINE__,
ROTATE_LEFT=__LINE__,
ROTATE_RIGHT=__LINE__,
STAIRS_DOWN=__LINE__,
STAIRS_UP=__LINE__,
START=__LINE__,
TICK=__LINE__,
TRAP=__LINE__,
UPDATE_SPRITE=__LINE__,
USE_ITEM=__LINE__,
};
}

420
src/game/autowalker.cpp Normal file
View file

@ -0,0 +1,420 @@
#include "game/autowalker.hpp"
#include "ai/ai_debug.hpp"
#include "game/level.hpp"
#include "game/systems.hpp"
struct InventoryStats {
int healing = 0;
int other = 0;
};
template<typename Comp>
int number_left() {
int count = 0;
auto world = GameDB::current_world();
auto player = GameDB::the_player();
world->query<components::Position, Comp>(
[&](const auto ent, auto&, auto&) {
if(ent != player) {
count++;
}
});
return count;
}
template<typename Comp>
Pathing compute_paths() {
auto& level = GameDB::current_level();
auto walls_copy = level.map->$walls;
Pathing paths{matrix::width(walls_copy), matrix::height(walls_copy)};
System::multi_path<Comp>(level, paths, walls_copy);
return paths;
}
DinkyECS::Entity Autowalker::camera_aim() {
auto& level = GameDB::current_level();
auto player_pos = GameDB::player_position();
// what happens if there's two things at that spot
if(level.collision->something_there(player_pos.aiming_at)) {
return level.collision->get(player_pos.aiming_at);
} else {
return DinkyECS::NONE;
}
}
void Autowalker::log(std::wstring msg) {
fsm.$map_ui.log(msg);
}
void Autowalker::status(std::wstring msg) {
fsm.$main_ui.$overlay_ui.show_text("bottom", msg);
}
void Autowalker::close_status() {
fsm.$main_ui.$overlay_ui.close_text("bottom");
}
Pathing Autowalker::path_to_enemies() {
return compute_paths<components::Combat>();
}
Pathing Autowalker::path_to_items() {
return compute_paths<components::Curative>();
}
void Autowalker::handle_window_events() {
fsm.$window.handleEvents(
[&](const sf::Event::KeyPressed &) {
fsm.autowalking = false;
close_status();
log(L"Aborting autowalk.");
},
[&](const sf::Event::MouseButtonPressed &) {
fsm.autowalking = false;
close_status();
log(L"Aborting autowalk.");
}
);
}
void Autowalker::process_combat() {
while(fsm.in_state(gui::State::IN_COMBAT)
|| fsm.in_state(gui::State::ATTACKING))
{
if(fsm.in_state(gui::State::ATTACKING)) {
send_event(game::Event::TICK);
} else {
send_event(game::Event::ATTACK);
}
}
}
void Autowalker::path_fail(const std::string& msg, Matrix& bad_paths, Point pos) {
dbc::log(msg);
status(L"PATH FAIL");
matrix::dump("MOVE FAIL PATHS", bad_paths, pos.x, pos.y);
log(L"Autowalk failed to find a path.");
send_event(game::Event::BOSS_START);
}
bool Autowalker::path_player(Pathing& paths, Point& target_out) {
auto& level = GameDB::current_level();
auto found = paths.find_path(target_out, PATHING_TOWARD, false);
if(found == PathingResult::FAIL) {
// failed to find a linear path, try diagonal
if(paths.find_path(target_out, PATHING_TOWARD, true) == PathingResult::FAIL) {
path_fail("random_walk", paths.$paths, target_out);
return false;
}
}
if(!level.map->can_move(target_out)) {
path_fail("level_map->can_move", paths.$paths, target_out);
return false;
}
return true;
}
void Autowalker::rotate_player(Point target) {
auto &player = GameDB::player_position();
if(target == player.location) {
dbc::log("player stuck at a locatoin");
fsm.autowalking = false;
return;
}
auto dir = System::shortest_rotate(player.location, player.aiming_at, target);
for(int i = 0; player.aiming_at != target; i++) {
if(i > 10) {
dbc::log("HIT OVER ROTATE BUG!");
break;
}
send_event(dir);
while(fsm.in_state(gui::State::ROTATING) ||
fsm.in_state(gui::State::COMBAT_ROTATE))
{
send_event(game::Event::TICK);
}
}
fsm.autowalking = player.aiming_at == target;
}
void Autowalker::update_state(ai::EntityAI& player_ai) {
int enemy_count = number_left<components::Combat>();
int item_count = number_left<components::InventoryItem>();
player_ai.set_state("no_more_enemies", enemy_count == 0);
player_ai.set_state("no_more_items", item_count == 0);
player_ai.set_state("enemy_found", found_enemy());
player_ai.set_state("health_good", player_health_good());
player_ai.set_state("in_combat",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
auto inv = player_item_count();
player_ai.set_state("have_item", inv.other > 0 || inv.healing > 0);
player_ai.set_state("have_healing", inv.healing > 0);
player_ai.update();
}
void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
ai::EntityAI player_ai("Host::actions", start, goal);
update_state(player_ai);
auto level = GameDB::current_level();
if(player_ai.wants_to("find_enemy")) {
status(L"FINDING ENEMY");
auto paths = path_to_enemies();
process_move(paths, [&](auto target) -> bool {
return level.collision->occupied(target);
});
face_enemy();
} else if(player_ai.wants_to("kill_enemy")) {
status(L"KILLING ENEMY");
if(fsm.in_state(gui::State::IN_COMBAT)) {
if(face_enemy()) {
process_combat();
}
}
} else if(player_ai.wants_to("use_healing")) {
status(L"USING HEALING");
player_use_healing();
} else if(player_ai.wants_to("collect_items") || player_ai.wants_to("find_healing")) {
fmt::println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
status(player_ai.wants_to("collect_items") ? L"COLLECTING ITEMS" : L"FIND HEALING");
player_ai.dump();
auto paths = path_to_items();
bool found_it = process_move(paths, [&](auto target) -> bool {
if(!level.collision->something_there(target)) return false;
auto entity = level.collision->get(target);
return level.world->has<components::Curative>(entity);
});
if(found_it) pickup_item();
fmt::println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
} else if(!player_ai.active()) {
close_status();
log(L"FINAL ACTION! Autowalk done.");
fsm.autowalking = false;
} else {
close_status();
dbc::log($F("Unknown action: {}", player_ai.to_string()));
}
}
void Autowalker::open_map() {
if(!map_opened_once) {
if(!fsm.$map_open) {
send_event(game::Event::MAP_OPEN);
map_opened_once = true;
}
}
}
void Autowalker::close_map() {
if(fsm.$map_open) {
send_event(game::Event::MAP_OPEN);
}
}
void Autowalker::autowalk() {
handle_window_events();
if(!fsm.autowalking) {
close_status();
return;
}
open_map();
face_enemy();
int move_attempts = 0;
auto start = ai::load_state("Host::initial_state");
auto goal = ai::load_state("Host::final_state");
do {
handle_window_events();
handle_player_walk(start, goal);
close_map();
move_attempts++;
} while(move_attempts < 100 && fsm.autowalking);
}
bool Autowalker::process_move(Pathing& paths, std::function<bool(Point)> is_that_it) {
// target has to start at the player location then...
auto target_out = GameDB::player_position().location;
// ... target gets modified as an out parameter to find the path
if(!path_player(paths, target_out)) {
close_status();
log(L"No paths found, aborting autowalk.");
return false;
}
if(rayview->aiming_at != target_out) rotate_player(target_out);
bool found_it = is_that_it(target_out);
if(!found_it) {
send_event(game::Event::MOVE_FORWARD);
while(fsm.in_state(gui::State::MOVING)) send_event(game::Event::TICK);
}
return found_it;
}
bool Autowalker::found_enemy() {
auto& level = GameDB::current_level();
auto player = GameDB::player_position();
for(matrix::compass it{level.map->$walls, player.location.x, player.location.y}; it.next();) {
Point aim{it.x, it.y};
auto aimed_ent = level.collision->occupied_by(player.aiming_at);
if(aim != player.aiming_at || aimed_ent == DinkyECS::NONE) continue;
if(level.world->has<components::Combat>(aimed_ent)) return true;
}
return false;
}
bool Autowalker::found_item() {
auto world = GameDB::current_world();
auto aimed_at = camera_aim();
return aimed_at != DinkyECS::NONE && world->has<components::InventoryItem>(aimed_at);
}
void Autowalker::send_event(game::Event ev, std::any data) {
fsm.event(ev, data);
fsm.render();
fsm.handle_world_events();
}
bool Autowalker::player_health_good() {
auto world = GameDB::current_world();
auto player = GameDB::the_player();
auto combat = world->get<components::Combat>(player);
float health = float(combat.hp) / float(combat.max_hp);
return health > 0.5f;
}
InventoryStats Autowalker::player_item_count() {
InventoryStats stats;
auto& level = GameDB::current_level();
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r")) {
stats.other += 1;
stats.healing += 1;
}
if(inventory.has("pocket_l")) {
stats.other += 1;
stats.healing += 1;
}
return stats;
}
void Autowalker::player_use_healing() {
auto& level = GameDB::current_level();
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r")) {
auto gui_id = fsm.$status_ui.$gui.entity("pocket_r");
send_event(game::Event::USE_ITEM, gui_id);
}
if(inventory.has("pocket_l")) {
auto gui_id = fsm.$status_ui.$gui.entity("pocket_l");
send_event(game::Event::USE_ITEM, gui_id);
}
}
void Autowalker::start_autowalk() {
fsm.autowalking = true;
}
void Autowalker::face_target(Point target) {
if(rayview->aiming_at != target) rotate_player(target);
}
bool Autowalker::face_enemy() {
auto& level = GameDB::current_level();
auto player_at = GameDB::player_position();
auto [found, neighbors] = level.collision->neighbors(player_at.location, true);
if(found) {
auto enemy_pos = level.world->get<components::Position>(neighbors[0]);
face_target(enemy_pos.location);
} else {
dbc::log("No enemies nearby, moving on.");
}
return found;
}
void Autowalker::click_inventory(const std::string& name, guecs::Modifiers mods) {
auto& cell = fsm.$status_ui.$gui.cell_for(name);
fsm.$status_ui.mouse(cell.mid_x, cell.mid_y, mods);
fsm.handle_world_events();
}
void Autowalker::pocket_potion(GameDB::Level &level) {
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r") && inventory.has("pocket_l")) {
player_use_healing();
}
send_event(game::Event::AIM_CLICK);
if(inventory.has("pocket_r")) {
click_inventory("pocket_l", {1 << guecs::ModBit::left});
} else {
click_inventory("pocket_r", {1 << guecs::ModBit::left});
}
}
void Autowalker::pickup_item() {
auto& level = GameDB::current_level();
auto& player_pos = GameDB::player_position();
auto collision = level.collision;
if(collision->something_there(player_pos.aiming_at)) {
auto entity = collision->get(player_pos.aiming_at);
fmt::println("AIMING AT entity {} @ {},{}",
entity, player_pos.aiming_at.x, player_pos.aiming_at.y);
if(level.world->has<components::Curative>(entity)) {
pocket_potion(level);
status(L"A POTION");
} else {
send_event(game::Event::AIM_CLICK);
status(L"I DON'T KNOW");
}
}
}

51
src/game/autowalker.hpp Normal file
View file

@ -0,0 +1,51 @@
#pragma once
#include "ai/ai.hpp"
#include "gui/fsm.hpp"
#include <guecs/ui.hpp>
struct InventoryStats;
struct Autowalker {
int enemy_count = 0;
int item_count = 0;
int device_count = 0;
bool map_opened_once = false;
gui::FSM& fsm;
std::shared_ptr<Raycaster> rayview;
Autowalker(gui::FSM& fsm)
: fsm(fsm), rayview(fsm.$main_ui.$rayview) {}
void autowalk();
void start_autowalk();
void open_map();
void close_map();
bool found_enemy();
bool found_item();
void handle_window_events();
void handle_player_walk(ai::State& start, ai::State& goal);
void send_event(game::Event ev, std::any data={});
void process_combat();
bool process_move(Pathing& paths, std::function<bool(Point)> cb);
bool path_player(Pathing& paths, Point &target_out);
void path_fail(const std::string& msg, Matrix& bad_paths, Point pos);
void rotate_player(Point target);
void log(std::wstring msg);
void status(std::wstring msg);
void close_status();
bool player_health_good();
void player_use_healing();
InventoryStats player_item_count();
void update_state(ai::EntityAI& player_ai);
DinkyECS::Entity camera_aim();
Pathing path_to_enemies();
Pathing path_to_items();
void face_target(Point target);
bool face_enemy();
void pickup_item();
void pocket_potion(GameDB::Level &level);
void click_inventory(const std::string& name, guecs::Modifiers mods);
};

36
src/game/components.cpp Normal file
View file

@ -0,0 +1,36 @@
#include "game/components.hpp"
#include "algos/point.hpp"
namespace components {
static ComponentMap MAP;
static bool MAP_configured = false;
void configure_entity(DinkyECS::World& world, DinkyECS::Entity ent, json& data) {
for (auto &i : data) {
dbc::check(i.contains("_type") && i["_type"].is_string(), $F("component has no _type: {}", data.dump()));
dbc::check(MAP.contains(i["_type"]), $F("MAP doesn't have type {}", std::string(i["_type"])));
MAP.at(i["_type"])(world, ent, i);
}
}
void init() {
if(!MAP_configured) {
components::enroll<AnimatedScene>(MAP);
components::enroll<Storyboard>(MAP);
components::enroll<Combat>(MAP);
components::enroll<Position>(MAP);
components::enroll<Curative>(MAP);
components::enroll<EnemyConfig>(MAP);
components::enroll<Personality>(MAP);
components::enroll<Tile>(MAP);
components::enroll<Motion>(MAP);
components::enroll<LightSource>(MAP);
components::enroll<Device>(MAP);
components::enroll<Sprite>(MAP);
components::enroll<Sound>(MAP);
components::enroll<Collision>(MAP);
MAP_configured = true;
}
}
}

193
src/game/components.hpp Normal file
View file

@ -0,0 +1,193 @@
#pragma once
#include "game/config.hpp"
#include "constants.hpp"
#include "algos/dinkyecs.hpp"
#include "algos/point.hpp"
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/System/Vector2.hpp>
#include <functional>
#include <optional>
#include "game/json_mods.hpp"
#include "ai/goap.hpp"
#include <array>
namespace combat {
enum class BattleHostState;
}
namespace components {
using std::string;
using namespace nlohmann;
struct CombatResult {
DinkyECS::Entity attacker;
combat::BattleHostState host_state;
int player_did = 0;
int enemy_did = 0;
};
struct InventoryItem {
int count;
json data;
};
struct SpriteEffect {
int frames;
std::shared_ptr<sf::Shader> effect;
};
struct Temporary {
bool is = true;
};
struct Collision {
bool has = true;
};
struct Position {
Point location{0,0};
Point aiming_at{0,0};
};
struct Motion {
int dx;
int dy;
bool random=false;
};
struct Tile {
wchar_t display;
std::string foreground;
std::string background;
};
struct GameConfig {
settings::Config game;
settings::Config enemies;
settings::Config items;
settings::Config tiles;
settings::Config devices;
};
struct Personality {
int hearing_distance = 10;
bool tough = true;
};
struct EnemyConfig {
std::string ai_script;
std::string ai_start_name;
std::string ai_goal_name;
};
struct Curative {
int hp = 10;
};
struct Sprite {
string name;
float scale;
};
struct AnimatedScene {
std::string background;
std::vector<std::string> layout;
json actors;
json fixtures;
};
struct Storyboard {
std::string image;
std::string audio;
std::vector<std::string> layout;
std::vector<std::array<std::string, 4>> beats;
};
struct Combat {
int hp;
int max_hp;
int ap_delta;
int max_ap;
int damage;
// everyone starts at 0 but ap_delta is added each round
int ap = 0;
/* NOTE: This is used to _mark_ entities as dead, to detect ones that have just died. Don't make attack automatically set it.*/
bool dead = false;
int attack(Combat &target);
};
struct LightSource {
int strength = 0;
float radius = 1.0f;
};
struct Device {
json config;
std::vector<std::string> events;
};
struct Sound {
std::string attack;
std::string death;
};
struct Player {
DinkyECS::Entity entity;
};
template <typename T> struct NameOf;
using ReflFuncSignature = std::function<void(DinkyECS::World& world, DinkyECS::Entity ent, nlohmann::json &j)>;
using ComponentMap = std::unordered_map<std::string, ReflFuncSignature>;
ENROLL_COMPONENT(Tile, display, foreground, background);
ENROLL_COMPONENT(AnimatedScene, background, layout, actors, fixtures);
ENROLL_COMPONENT(Sprite, name, scale);
ENROLL_COMPONENT(Curative, hp);
ENROLL_COMPONENT(LightSource, strength, radius);
ENROLL_COMPONENT(Position, location.x, location.y);
ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name);
ENROLL_COMPONENT(Personality, hearing_distance, tough);
ENROLL_COMPONENT(Motion, dx, dy, random);
ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, dead);
ENROLL_COMPONENT(Device, config, events);
ENROLL_COMPONENT(Storyboard, image, audio, layout, beats);
ENROLL_COMPONENT(Sound, attack, death);
ENROLL_COMPONENT(Collision, has);
template<typename COMPONENT> COMPONENT convert(nlohmann::json &data) {
COMPONENT result;
from_json(data, result);
return result;
}
template<typename COMPONENT> COMPONENT get(nlohmann::json &data) {
for (auto &i : data["components"]) {
if(i["_type"] == NameOf<COMPONENT>::name) {
return convert<COMPONENT>(i);
}
}
return {};
}
template <typename COMPONENT> void enroll(ComponentMap &m) {
m[NameOf<COMPONENT>::name] = [](DinkyECS::World& world, DinkyECS::Entity ent, nlohmann::json &j) {
COMPONENT c;
from_json(j, c);
world.set<COMPONENT>(ent, c);
};
}
void init();
void configure_entity(DinkyECS::World& world, DinkyECS::Entity ent, json& data);
}

68
src/game/config.cpp Normal file
View file

@ -0,0 +1,68 @@
#include "game/config.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
namespace settings {
using nlohmann::json;
std::filesystem::path Config::BASE_DIR{"."};
Config::Config(const std::string src_path) : $src_path(src_path) {
auto path_to = Config::path_to($src_path);
dbc::check(std::filesystem::exists(path_to),
$F("requested config file {} doesn't exist", path_to.string()));
std::ifstream infile(path_to);
$config = json::parse(infile);
}
nlohmann::json &Config::operator[](size_t key) {
return $config[key];
}
json &Config::operator[](const std::string &key) {
dbc::check($config.contains(key), $F("ERROR in config, key {} doesn't exist.", key));
return $config[key];
}
std::wstring Config::wstring(const std::string main_key, const std::string sub_key) {
dbc::check($config.contains(main_key),
$F("ERROR wstring main/key in config, main_key {} doesn't exist.", main_key));
dbc::check($config[main_key].contains(sub_key),
$F("ERROR wstring in config, main_key/key {}/{} doesn't exist.", main_key, sub_key));
const std::string& str_val = $config[main_key][sub_key];
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
return $converter.from_bytes(str_val);
}
std::vector<std::string> Config::keys() {
std::vector<std::string> the_fucking_keys;
for(auto& [key, value] : $config.items()) {
the_fucking_keys.push_back(key);
}
return the_fucking_keys;
}
void Config::set_base_dir(const char *optarg) {
Config::BASE_DIR.assign(optarg);
}
std::filesystem::path Config::path_to(const std::string& path) {
return Config::BASE_DIR / path;
}
Config get(const std::string& name) {
if(name.ends_with(".json")) {
return {name};
} else {
auto path = Config::BASE_DIR / fmt::format("assets/{}.json", name);
dbc::check(std::filesystem::exists(path),
$F("config file {} does not exist", path.string()));
return {path.string()};
}
}
}

29
src/game/config.hpp Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include <nlohmann/json.hpp>
#include <fstream>
#include <codecvt>
#include <filesystem>
namespace settings {
struct Config {
static std::filesystem::path BASE_DIR;
nlohmann::json $config;
std::string $src_path;
Config(const std::string src_path);
Config(nlohmann::json config, std::string src_path)
: $config(config), $src_path(src_path) {}
nlohmann::json &operator[](size_t);
nlohmann::json &operator[](const std::string &key);
nlohmann::json &json() { return $config; };
std::wstring wstring(const std::string main_key, const std::string sub_key);
std::vector<std::string> keys();
static void set_base_dir(const char *optarg);
static std::filesystem::path path_to(const std::string& path);
};
Config get(const std::string &name);
}

99
src/game/inventory.cpp Normal file
View file

@ -0,0 +1,99 @@
#include "game/inventory.hpp"
namespace inventory {
bool Model::add(const std::string in_slot, DinkyECS::Entity ent) {
// NOTE: for the C++ die hards, copy the in_slot on purpose to avoid dangling reference
if(by_slot.contains(in_slot) || by_entity.contains(ent)) return false;
by_entity.insert_or_assign(ent, in_slot);
by_slot.insert_or_assign(in_slot, ent);
invariant();
return true;
}
const std::string& Model::get(DinkyECS::Entity ent) {
return by_entity.at(ent);
}
DinkyECS::Entity Model::get(const std::string& slot) {
return by_slot.at(slot);
}
bool Model::has(DinkyECS::Entity ent) {
return by_entity.contains(ent);
}
bool Model::has(const std::string& slot) {
return by_slot.contains(slot);
}
void Model::remove(DinkyECS::Entity ent) {
dbc::check(by_entity.contains(ent), "attempt to remove entity that isn't in by_entity");
// NOTE: this was a reference but that caused corruption, just copy
auto slot = by_entity.at(ent);
dbc::log($F("removing entity {} and slot {}", ent, slot));
dbc::check(by_slot.contains(slot), "entity is in by_entity but the slot is not in by_slot");
// NOTE: you have to erase the entity after the slot or else you get corruption
by_slot.erase(slot);
by_entity.erase(ent);
invariant();
}
void Model::invariant() {
for(auto& [slot, ent] : by_slot) {
dbc::check(by_entity.contains(ent),
$F("entity {} in by_slot isn't in by_entity?", ent));
dbc::check(by_entity.at(ent) == slot,
$F("mismatched slot {} in by_slot doesn't match entity {}", slot, ent));
}
for(auto& [ent, slot] : by_entity) {
dbc::check(by_slot.contains(slot),
$F("slot {} in by_entity isn't in by_slot?", ent));
dbc::check(by_slot.at(slot) == ent,
$F("mismatched entity {} in by_entity doesn't match entity {}", ent, slot));
}
dbc::check(by_slot.size() == by_entity.size(), "by_slot and by_entity have differing sizes");
}
void Model::dump() {
invariant();
fmt::println("INVENTORY has {} slots, sizes equal? {}, contents:",
by_entity.size(), by_entity.size() == by_slot.size());
for(auto [slot, ent] : by_slot) {
fmt::println("slot={}, ent={}, both={}, equal={}",
slot, ent, by_entity.contains(ent), by_entity.at(ent) == slot);
}
}
void Model::swap(DinkyECS::Entity a_ent, DinkyECS::Entity b_ent) {
dbc::check(by_entity.contains(a_ent), "a_entity not in inventory");
dbc::check(by_entity.contains(b_ent), "b_entity not in inventory");
if(a_ent == b_ent) return;
auto a_slot = get(a_ent);
auto b_slot = get(b_ent);
dbc::check(a_slot != b_slot, "somehow I got two different entities but they gave the same slot?");
by_slot.insert_or_assign(a_slot, b_ent);
by_entity.insert_or_assign(b_ent, a_slot);
by_slot.insert_or_assign(b_slot, a_ent);
by_entity.insert_or_assign(a_ent, b_slot);
}
size_t Model::count() {
dbc::check(by_slot.size() == by_entity.size(), "entity and slot maps have different sizes");
return by_entity.size();
}
}

25
src/game/inventory.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "algos/dinkyecs.hpp"
#include <unordered_map>
// BUG: this should have a bool for "permanent" or "constant" so that
// everything working with it knows to do the make_constant/not_constant
// dance when using it. Idea is the System:: ops for this would get it
// and then look at the bool and add the constant ops as needed.
namespace inventory {
struct Model {
std::unordered_map<std::string, DinkyECS::Entity> by_slot;
std::unordered_map<DinkyECS::Entity, std::string> by_entity;
bool add(const std::string in_slot, DinkyECS::Entity ent);
const std::string& get(DinkyECS::Entity ent);
DinkyECS::Entity get(const std::string& slot);
bool has(DinkyECS::Entity ent);
bool has(const std::string& slot);
void remove(DinkyECS::Entity ent);
void invariant();
void dump();
void swap(DinkyECS::Entity a_ent, DinkyECS::Entity b_ent);
size_t count();
};
}

45
src/game/json_mods.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <nlohmann/json.hpp>
#include <nlohmann/json_fwd.hpp>
#include <optional>
#define ENROLL_COMPONENT(COMPONENT, ...) \
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(COMPONENT, __VA_ARGS__); \
template <> struct NameOf<COMPONENT> { \
static constexpr const char *name = #COMPONENT; \
};
// partial specialization (full specialization works too)
namespace nlohmann {
template <typename T>
struct adl_serializer<std::optional<T>> {
static void to_json(json& j, const std::optional<T>& opt) {
if (opt == std::nullopt) {
j = nullptr;
} else {
j = *opt; // this will call adl_serializer<T>::to_json which will
// find the free function to_json in T's namespace!
}
}
static void from_json(const json& j, std::optional<T>& opt) {
if (j.is_null() || j == false) {
opt = std::nullopt;
} else {
opt = std::make_optional<T>(j.template get<T>());
// same as above, but with adl_serializer<T>::from_json
}
}
};
template<>
struct adl_serializer<std::chrono::milliseconds> {
static void to_json(json& j, const std::chrono::milliseconds& opt) {
j = opt.count();
}
static void from_json(const json& j, std::chrono::milliseconds& opt) {
opt = std::chrono::milliseconds{int(j)};
}
};
}

134
src/game/level.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "game/level.hpp"
#include "game/components.hpp"
#include "game/worldbuilder.hpp"
#include "constants.hpp"
#include "game/systems.hpp"
#include "graphics/textures.hpp"
#include <list>
using lighting::LightRender;
using std::shared_ptr, std::make_shared;
using namespace components;
struct LevelScaling {
int map_width=INITIAL_MAP_W;
int map_height=INITIAL_MAP_H;
};
namespace GameDB {
using std::shared_ptr, std::string, std::make_shared;
struct LevelDB {
std::list<GameDB::Level> levels;
Level* current_level = nullptr;
int level_count = 0;
};
shared_ptr<LevelDB> LDB = nullptr;
bool initialized = false;
LevelScaling scale_level() {
return {
INITIAL_MAP_W + int(LDB->level_count * 2),
INITIAL_MAP_H + int(LDB->level_count * 2)
};
}
shared_ptr<DinkyECS::World> clone_load_world(shared_ptr<DinkyECS::World> prev_world) {
auto world = make_shared<DinkyECS::World>();
if(prev_world == nullptr) {
GameDB::load_configs(*world);
} else {
prev_world->clone_into(*world);
}
return world;
}
void register_level(Level level) {
// size BEFORE push to get the correct index
level.index = LDB->levels.size();
LDB->levels.push_back(level);
dbc::check(level.index == LDB->levels.size() - 1, "Level index is not the same as LDB->levels.size() - 1, off by one error");
LDB->level_count = level.index;
LDB->current_level = &LDB->levels.back();
}
void new_level(std::shared_ptr<DinkyECS::World> prev_world) {
dbc::check(initialized, "Forgot to call GameDB::init()");
auto world = clone_load_world(prev_world);
auto scaling = scale_level();
auto map = make_shared<Map>(scaling.map_width, scaling.map_height);
auto collision = std::make_shared<SpatialMap>();
WorldBuilder builder(*map, *collision);
builder.generate(*world);
auto lights = make_shared<LightRender>(map->tiles());
auto player = world->get_the<Player>();
register_level({
.player=player.entity,
.map=map,
.world=world,
.lights=lights,
.collision=collision});
}
void init() {
components::init();
textures::init();
if(!initialized) {
LDB = make_shared<LevelDB>();
initialized = true;
new_level(NULL);
}
}
shared_ptr<DinkyECS::World> current_world() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return current_level().world;
}
Level& create_level() {
dbc::check(initialized, "Forgot to call GameDB::init()");
new_level(current_world());
return LDB->levels.back();
}
Level &current_level() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return *LDB->current_level;
}
components::Position& player_position() {
dbc::check(initialized, "Forgot to call GameDB::init()");
auto& level = current_level();
return level.world->get<components::Position>(level.player);
}
DinkyECS::Entity the_player() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return current_level().player;
}
void load_configs(DinkyECS::World &world) {
world.set_the<GameConfig>({
settings::get("config"),
settings::get("enemies"),
settings::get("items"),
settings::get("tiles"),
settings::get("devices"),
});
}
}

34
src/game/level.hpp Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include "algos/dinkyecs.hpp"
#include "graphics/lights.hpp"
#include "game/map.hpp"
#include <memory>
#include "algos/spatialmap.hpp"
namespace components {
struct Position;
}
namespace GameDB {
struct Level {
size_t index = 0;
DinkyECS::Entity player = DinkyECS::NONE;
std::shared_ptr<Map> map = nullptr;
std::shared_ptr<DinkyECS::World> world = nullptr;
std::shared_ptr<lighting::LightRender> lights = nullptr;
std::shared_ptr<SpatialMap> collision = nullptr;
};
Level& create_level();
void init();
Level &current_level();
std::shared_ptr<DinkyECS::World> current_world();
components::Position& player_position();
DinkyECS::Entity the_player();
std::shared_ptr<DinkyECS::World> clone_load_world(std::shared_ptr<DinkyECS::World> prev_world);
void load_configs(DinkyECS::World &world);
void register_level(Level level);
}

132
src/game/map.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "game/map.hpp"
#include "dbc.hpp"
#include "algos/rand.hpp"
#include <vector>
#include <array>
#include <fmt/core.h>
#include <utility>
#include "algos/matrix.hpp"
using std::vector, std::pair;
using namespace fmt;
Map::Map(size_t width, size_t height) :
$width(width),
$height(height),
$walls(height, matrix::Row(width, SPACE_VALUE)),
$paths(width, height)
{}
Map::Map(Matrix &walls, Pathing &paths) :
$walls(walls),
$paths(paths)
{
$width = matrix::width(walls);
$height = matrix::height(walls);
}
void Map::make_paths() {
INVARIANT();
$paths.compute_paths($walls);
}
bool Map::inmap(size_t x, size_t y) {
return x < $width && y < $height;
}
void Map::set_target(const Point &at, int value) {
$paths.set_target(at, value);
}
void Map::clear_target(const Point &at) {
$paths.clear_target(at);
}
bool Map::place_entity(size_t room_index, Point &out) {
dbc::check($dead_ends.size() != 0, "no dead ends?!");
if(room_index < $rooms.size()) {
Room &start = $rooms.at(room_index);
for(matrix::rando_rect it{$walls, start.x, start.y, start.width, start.height}; it.next();) {
if(!iswall(it.x, it.y)) {
out.x = it.x;
out.y = it.y;
return true;
}
}
}
out = $dead_ends.at(room_index % $dead_ends.size());
return true;
}
bool Map::iswall(size_t x, size_t y) {
return !$doors.contains({x, y}) && $walls[y][x] == WALL_VALUE;
}
void Map::dump(int show_x, int show_y) {
matrix::dump("WALLS", walls(), show_x, show_y);
matrix::dump("PATHS", paths(), show_x, show_y);
}
bool Map::can_move(Point move_to) {
return inmap(move_to.x, move_to.y) && !iswall(move_to.x, move_to.y);
}
Point Map::map_to_camera(const Point &loc, const Point &cam_orig) {
return {loc.x - cam_orig.x, loc.y - cam_orig.y};
}
Point Map::center_camera(const Point &around, size_t view_x, size_t view_y) {
int high_x = int(width() - view_x);
int high_y = int(height() - view_y);
int center_x = int(around.x - view_x / 2);
int center_y = int(around.y - view_y / 2);
size_t start_x = high_x > 0 ? std::clamp(center_x, 0, high_x) : 0;
size_t start_y = high_y > 0 ? std::clamp(center_y, 0, high_y) : 0;
return {start_x, start_y};
}
bool Map::random_walk(Point &out, bool random, int direction) {
int choice = Random::uniform(0,4);
return $paths.find_path(out, direction, random && choice == 0) != PathingResult::FAIL;
}
bool Map::INVARIANT() {
using dbc::check;
check($walls.size() == height(), "walls wrong height");
check($walls[0].size() == width(), "walls wrong width");
check($paths.$width == width(), "in Map paths width don't match map width");
check($paths.$height == height(), "in Map paths height don't match map height");
for(auto room : $rooms) {
check(int(room.x) >= 0 && int(room.y) >= 0,
$F("room invalid position {},{}",
room.x, room.y));
check(int(room.width) > 0 && int(room.height) > 0,
$F("room has invalid dims {},{}",
room.width, room.height));
}
return true;
}
void Map::init_tiles() {
$tiles = $walls;
}
void Map::add_room(Room &room) {
$rooms.push_back(room);
}
void Map::invert_space() {
for(matrix::each_cell it{$walls}; it.next();) {
int is_wall = !$walls[it.y][it.x];
$walls[it.y][it.x] = is_wall;
}
}

96
src/game/map.hpp Normal file
View file

@ -0,0 +1,96 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <random>
#include <algorithm>
#include <fmt/core.h>
#include "algos/point.hpp"
#include "graphics/lights.hpp"
#include "algos/pathing.hpp"
#include "algos/matrix.hpp"
#include "constants.hpp"
using lighting::LightSource;
struct Room {
size_t x = 0;
size_t y = 0;
size_t width = 0;
size_t height = 0;
bool contains(Point at) {
return at.x >= x
&& at.x <= x + width -1
&& at.y >= y
&& at.y <= y + height - 1;
}
bool overlaps(Room other) {
return
// other left > this right == other too far right
!( other.x > x + width
// other right < this left == other too far left
|| other.x + other.width < x
// other top > this bottom == too far below
|| other.y > y + height
// other bottom < this top == too far above
|| other.y + other.height < y);
}
bool operator==(const Room&) const = default;
};
using EntityGrid = std::unordered_map<Point, wchar_t>;
class Map {
public:
size_t $width;
size_t $height;
Matrix $walls;
Matrix $tiles;
Pathing $paths;
std::vector<Room> $rooms;
std::vector<Point> $dead_ends;
std::unordered_map<Point, bool> $doors;
Map(size_t width, size_t height);
Map(Matrix &walls, Pathing &paths);
Matrix& paths() { return $paths.paths(); }
Matrix& input_map() { return $paths.input(); }
Matrix& walls() { return $walls; }
Matrix& tiles() { return $tiles; }
std::vector<Room>& rooms() { return $rooms; }
size_t width() { return $width; }
size_t height() { return $height; }
int distance(Point to) { return $paths.distance(to); }
Room &room(size_t at) { return $rooms[at]; }
size_t room_count() { return $rooms.size(); }
bool place_entity(size_t room_index, Point &out);
bool inmap(size_t x, size_t y);
bool iswall(size_t x, size_t y);
bool can_move(Point move_to);
bool random_walk(Point &out, bool random=false, int direction=PATHING_TOWARD);
void make_paths();
void set_target(const Point &at, int value=0);
void clear_target(const Point &at);
Point map_to_camera(const Point &loc, const Point &cam_orig);
Point center_camera(const Point &around, size_t view_x, size_t view_y);
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
void init_tiles();
void add_room(Room &room);
void invert_space();
};

81
src/game/sound.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "game/sound.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
#include "game/config.hpp"
namespace sound {
static SoundManager SMGR;
static bool initialized = false;
static bool muted = false;
using namespace fmt;
using std::make_shared;
namespace fs = std::filesystem;
SoundPair& get_sound_pair(const std::string& name) {
dbc::check(initialized, "You need to call sound::init() first");
if(SMGR.sounds.contains(name)) {
// get the sound from the sound map
return SMGR.sounds.at(name);
} else {
dbc::log($F("Attempted to stop {} sound but not available.", name));
return SMGR.sounds.at("blank");
}
}
void init() {
if(!initialized) {
auto assets = settings::get("config");
for(auto& el : assets["sounds"].items()) {
load(el.key(), el.value());
}
initialized = true;
}
}
void load(const std::string& name, const std::string& sound_path) {
dbc::check(fs::exists(sound_path), $F("sound file {} does not exist", sound_path));
// create the buffer and keep in the buffer map
auto buffer = make_shared<sf::SoundBuffer>(sound_path);
// set it on the sound and keep in the sound map
auto sound = make_shared<sf::Sound>(*buffer);
sound->setRelativeToListener(false);
sound->setPosition({0.0f, 0.0f, 1.0f});
SMGR.sounds.try_emplace(name, buffer, sound);
}
void play(const std::string& name, bool loop) {
if(muted) return;
auto& pair = get_sound_pair(name);
pair.sound->setLooping(loop);
// play it
pair.sound->play();
}
void stop(const std::string& name) {
auto& pair = get_sound_pair(name);
pair.sound->stop();
}
bool playing(const std::string& name) {
auto& pair = get_sound_pair(name);
auto status = pair.sound->getStatus();
return status == sf::SoundSource::Status::Playing;
}
void play_at(const std::string& name, float x, float y, float z) {
auto& pair = get_sound_pair(name);
pair.sound->setPosition({x, y, z});
pair.sound->play();
}
void mute(bool setting) {
muted = setting;
}
}

26
src/game/sound.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <filesystem>
#include <memory>
#include <unordered_map>
#include <SFML/Audio.hpp>
namespace sound {
struct SoundPair {
std::shared_ptr<sf::SoundBuffer> buffer;
std::shared_ptr<sf::Sound> sound;
};
struct SoundManager {
std::unordered_map<std::string, SoundPair> sounds;
};
void init();
void load(const std::string& name, const std::string& path);
void play(const std::string& name, bool loop=false);
void play_at(const std::string& name, float x, float y, float z);
void stop(const std::string& name);
void mute(bool setting);
bool playing(const std::string& name);
SoundPair& get_sound_pair(const std::string& name);
}

656
src/game/systems.cpp Normal file
View file

@ -0,0 +1,656 @@
#include "game/systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#include "algos/rand.hpp"
#include "algos/spatialmap.hpp"
#include "dbc.hpp"
#include "graphics/lights.hpp"
#include "events.hpp"
#include "game/sound.hpp"
#include "ai/ai.hpp"
#include "ai/ai_debug.hpp"
#include "algos/shiterator.hpp"
#include "combat/battle.hpp"
#include <iostream>
#include "graphics/shaders.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/level.hpp"
#include "events.hpp"
#include "graphics/animation.hpp"
using std::string;
using namespace fmt;
using namespace combat;
using namespace components;
using namespace DinkyECS;
using lighting::LightSource;
void System::set_position(World& world, SpatialMap& collision, Entity entity, Position pos) {
world.set<Position>(entity, pos);
bool has_collision = world.has<Collision>(entity);
collision.insert(pos.location, entity, has_collision);
}
void System::lighting() {
auto& level = GameDB::current_level();
auto& light = *level.lights;
auto& world = *level.world;
auto& map = *level.map;
light.reset_light();
world.query<Position>([&](auto, auto &position) {
light.set_light_target(position.location);
});
light.path_light(map.walls());
world.query<Position, LightSource>([&](auto ent, auto &position, auto &lightsource) {
light.render_light(lightsource, position.location);
if(ent == level.player) {
light.update_fow(position.location, lightsource);
}
});
}
void System::generate_paths() {
auto& level = GameDB::current_level();
const auto &player_pos = GameDB::player_position();
level.map->set_target(player_pos.location);
level.map->make_paths();
}
void System::enemy_ai_initialize() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
world.query<Position, EnemyConfig>([&](const auto ent, auto& pos, auto& config) {
if(world.has<ai::EntityAI>(ent)) {
auto& enemy = world.get<ai::EntityAI>(ent);
auto& personality = world.get<Personality>(ent);
enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance);
enemy.update();
} else {
auto ai_start = ai::load_state(config.ai_start_name);
auto ai_goal = ai::load_state(config.ai_goal_name);
ai::EntityAI enemy(config.ai_script, ai_start, ai_goal);
auto& personality = world.get<Personality>(ent);
enemy.set_state("tough_personality", personality.tough);
enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance);
enemy.update();
world.set<ai::EntityAI>(ent, enemy);
}
});
}
void System::enemy_pathing() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
const auto &player_pos = GameDB::player_position();
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
if(ent != level.player) {
auto& enemy_ai = world.get<ai::EntityAI>(ent);
Point out = position.location; // copy
bool found_path = false;
if(enemy_ai.wants_to("find_enemy")) {
found_path = map.random_walk(out, motion.random, PATHING_TOWARD);
} else if(enemy_ai.wants_to("run_away")) {
found_path = map.random_walk(out, motion.random, PATHING_AWAY);
} else {
motion = {0,0};
return; // enemy doesn't want to move
}
enemy_ai.set_state("cant_move", !found_path);
enemy_ai.update();
motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
}
});
map.clear_target(player_pos.location);
}
void System::motion() {
auto& level = GameDB::current_level();
auto world = level.world;
auto map = level.map;
auto collider = level.collision;
world->query<Position, Motion>(
[&](auto ent, auto &position, auto &motion) {
// skip enemies that aren't moving
if(motion.dx == 0 && motion.dy == 0) return;
Point move_to = {
position.location.x + motion.dx,
position.location.y + motion.dy
};
motion = {0,0}; // clear it after getting it
dbc::check(map->can_move(move_to), "Enemy pathing failed, move_to is wall.");
bool cant_move = collider->occupied(move_to);
if(auto enemy_ai = world->get_if<ai::EntityAI>(ent)) {
enemy_ai->set_state("cant_move", cant_move);
}
// it's a wall, skip
if(cant_move) return;
// all good, do the move
collider->move(position.location, move_to, ent);
position.location = move_to;
});
}
void System::distribute_loot(Position target_pos) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& config = world.get_the<GameConfig>();
int inventory_count = Random::uniform(0, 3);
auto loot_entity = world.entity();
if(inventory_count > 0) {
auto& entity_data = config.devices["DEAD_BODY_LOOTABLE"];
components::configure_entity(world, loot_entity, entity_data["components"]);
// BUG: inventory_count here isn't really used to remove it
world.set<InventoryItem>(loot_entity, {inventory_count, entity_data});
} else {
// this creates a dead body on the ground
auto& entity_data = config.devices["DEAD_BODY"];
components::configure_entity(world, loot_entity, entity_data["components"]);
}
set_position(world, *level.collision, loot_entity, target_pos);
level.world->send<game::Event>(game::Event::ENTITY_SPAWN, loot_entity, {});
}
void System::death() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto player = world.get_the<Player>();
std::vector<Entity> dead_things;
world.query<Combat>([&](auto ent, auto &combat) {
// bring out yer dead
if(combat.hp <= 0 && !combat.dead) {
combat.dead = true;
if(ent != player.entity) {
// we won't change out the player's components later
dead_things.push_back(ent);
}
// we need to send this event for everything that dies
world.send<game::Event>(game::Event::DEATH, ent, {});
} else if(float(combat.hp) / float(combat.max_hp) < 0.5f) {
// if enemies are below 50% health they are marked with bad health
if(world.has<ai::EntityAI>(ent)) {
auto& enemy_ai = world.get<ai::EntityAI>(ent);
enemy_ai.set_state("health_good", false);
enemy_ai.update();
}
}
});
// this goes through everything that died and changes them to a gravestone
for(auto ent : dead_things) {
if(auto snd = world.get_if<Sound>(ent)) {
sound::stop(snd->attack);
sound::play(snd->death);
}
auto pos = world.get<Position>(ent);
// need to remove _after_ getting the position
level.collision->remove(pos.location, ent);
// distribute_loot is then responsible for putting something there
System::distribute_loot(pos);
world.destroy(ent);
}
}
void System::combat(int attack_id) {
auto& level = GameDB::current_level();
auto& collider = *level.collision;
auto& world = *level.world;
const auto& player_pos = GameDB::player_position();
auto& player_combat = world.get<Combat>(level.player);
auto& player_ai = world.get<ai::EntityAI>(level.player);
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_pos.location);
combat::BattleEngine battle;
if(found) {
for(auto entity : nearby) {
if(world.has<ai::EntityAI>(entity)) {
auto& enemy_ai = world.get<ai::EntityAI>(entity);
auto& enemy_combat = world.get<Combat>(entity);
battle.add_enemy({entity, &enemy_ai, &enemy_combat});
}
}
battle.add_enemy({level.player, &player_ai, &player_combat, true});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.player_request("kill_enemy");
battle.ap_refresh();
battle.plan();
}
// battle.dump();
while(auto act = battle.next()) {
auto [enemy, enemy_action, cost, host_state] = *act;
// player shouldn't hit theirself
if(host_state != BattleHostState::not_host) continue;
components::CombatResult result {
.attacker=enemy.entity,
.host_state=host_state,
.player_did=player_combat.attack(*enemy.combat),
.enemy_did=0
};
if(result.player_did > 0) {
spawn_attack(world, attack_id, enemy.entity);
}
if(enemy_action == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
animation::animate_entity(world, enemy.entity);
}
world.send<game::Event>(game::Event::COMBAT, enemy.entity, result);
}
}
void System::collision() {
auto& level = GameDB::current_level();
auto& collider = *level.collision;
auto& world = *level.world;
const auto& player_pos = GameDB::player_position();
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_pos.location);
int combat_count = 0;
// AI: I think also this would a possible place to run AI decisions
for(auto entity : nearby) {
if(world.has<Combat>(entity)) {
auto combat = world.get<Combat>(entity);
if(!combat.dead) {
combat_count++;
world.send<game::Event>(game::Event::COMBAT_START, entity, entity);
}
} else {
dbc::log($F("UNKNOWN COLLISION TYPE {}", entity));
}
}
if(combat_count == 0) {
// BUG: this is probably how we get stuck in combat
world.send<game::Event>(game::Event::NO_NEIGHBORS, level.player, level.player);
}
}
/*
* This isn't for destroying something, but just removing it
* from the world for say, putting into a container or inventory.
*/
void System::remove_from_world(Entity entity) {
auto& level = GameDB::current_level();
auto& item_pos = level.world->get<Position>(entity);
level.collision->remove(item_pos.location, entity);
// if you don't do this you get the bug that you can pickup
// an item and it'll also be in your inventory
level.world->remove<Position>(entity);
}
void System::pickup() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& collision = *level.collision;
auto pos = GameDB::player_position();
if(!collision.something_there(pos.aiming_at)) return;
auto entity = level.collision->find(pos.aiming_at, [&](auto data) -> bool {
return (world.has<InventoryItem>(data.entity) ||
world.has<Device>(data.entity));
});
if(entity == DinkyECS::NONE) {
dbc::log("no inventory or devices there");
return;
}
// use spatial find to find an item with inventory...
if(world.has<InventoryItem>(entity)) {
// NOTE: this might need to be a separate system so that people can leave stuff alone
remove_from_world(entity);
// NOTE: chests are different from say a torch, maybe 2 events or the
// GUI figures out which it is, then when you click either pick it up
// and move it or show the loot container UI
world.send<game::Event>(game::Event::LOOT_ITEM, entity, entity);
} else if(world.has<Device>(entity)) {
System::device(world, level.player, entity);
} else {
dbc::log("BUG: is this a bug in pickup?!");
}
}
void System::device(World &world, Entity actor, Entity item) {
auto& device = world.get<Device>(item);
dbc::log($F("entity {} INTERACTED WITH DEVICE {}", actor, item));
for(auto event : device.events) {
if(event == "STAIRS_DOWN") {
world.send<game::Event>(game::Event::STAIRS_DOWN, actor, device);
} else if(event == "STAIRS_UP") {
world.send<game::Event>(game::Event::STAIRS_UP, actor, device);
} else if(event == "TRAP") {
world.send<game::Event>(game::Event::TRAP, actor, device);
} else if(event == "LOOT_CONTAINER") {
world.send<game::Event>(game::Event::LOOT_CONTAINER, actor, device);
} else {
dbc::log($F(
"INVALID EVENT {} for device {}",
event, (std::string)device.config["name"]));
}
}
}
void System::move_player(Position move_to) {
auto& level = GameDB::current_level();
auto old_pos = level.world->get<Position>(level.player);
level.world->set<Position>(level.player, move_to);
level.collision->move(old_pos.location, move_to.location, level.player);
}
void System::player_status() {
auto& level = GameDB::current_level();
auto& combat = level.world->get<Combat>(level.player);
float percent = float(combat.hp) / float(combat.max_hp);
if(percent > 0.8) {
sound::play("hp_status_80");
} else if(percent > 0.6) {
sound::play("hp_status_60");
} else if(percent > 0.3) {
sound::play("hp_status_30");
} else if(percent > 0.1) {
sound::play("hp_status_10");
} else {
sound::play("hp_status_00");
}
}
std::shared_ptr<sf::Shader> System::sprite_effect(Entity entity) {
auto world = GameDB::current_world();
if(auto se = world->get_if<SpriteEffect>(entity)) {
if(se->frames > 0) {
se->frames--;
return se->effect;
} else {
world->remove<SpriteEffect>(entity);
return nullptr;
}
} else {
return nullptr;
}
}
Entity System::spawn_item(World& world, const std::string& name) {
auto& config = world.get_the<GameConfig>().items;
auto& item_config = config[name];
auto item_id = world.entity();
world.set<InventoryItem>(item_id, {1, item_config});
components::configure_entity(world, item_id, item_config["components"]);
return item_id;
}
void System::drop_item(Entity item) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
auto player_pos = GameDB::player_position();
dbc::check(map.can_move(player_pos.location), "impossible, the player can't be in a wall");
Position drop_spot = {player_pos.aiming_at.x, player_pos.aiming_at.y};
// if they're aiming at a wall then drop at their feet
if(!map.can_move(drop_spot.location)) drop_spot = player_pos;
set_position(world, *level.collision, item, drop_spot);
level.world->not_constant(item);
level.world->send<game::Event>(game::Event::ENTITY_SPAWN, item, {});
}
// NOTE: I think pickup and this need to be different
bool System::place_in_container(Entity cont_id, const std::string& name, Entity world_entity) {
auto world = GameDB::current_world();
auto& container = world->get<inventory::Model>(cont_id);
if(container.has(world_entity)) {
fmt::println("container {} already has entity {}, skip", cont_id, world_entity);
// NOTE: I think this would be a move?!
return false;
} else if(container.has(name)) {
// this is an already occupied slot
fmt::println("container {} already has SLOT {}, skip", cont_id, name);
return false;
} else {
// this should only apply to the player's inventory
fmt::println("adding {} entity to loot with name {}", world_entity, name);
container.add(name, world_entity);
return true;
}
}
void System::remove_from_container(Entity cont_id, const std::string& slot_id) {
auto world = GameDB::current_world();
auto& container = world->get<inventory::Model>(cont_id);
auto entity = container.get(slot_id);
container.remove(entity);
}
void System::inventory_swap(Entity container_id, const std::string& a_name, const std::string &b_name) {
auto& level = GameDB::current_level();
dbc::check(a_name != b_name, "Attempt to inventory swap the same slot, you should check this and avoid calling me.");
auto& inventory = level.world->get<inventory::Model>(container_id);
auto a_ent = inventory.get(a_name);
auto b_ent = inventory.get(b_name);
inventory.swap(a_ent, b_ent);
}
bool System::inventory_occupied(Entity container_id, const std::string& name) {
auto world = GameDB::current_world();
auto& inventory = world->get<inventory::Model>(container_id);
return inventory.has(name);
}
void System::draw_map(Matrix& grid, EntityGrid& entity_map) {
auto& level = GameDB::current_level();
auto& world = *level.world;
Map &map = *level.map;
Matrix &fow = level.lights->$fow;
size_t view_x = matrix::width(grid) - 1;
size_t view_y = matrix::height(grid) - 1;
entity_map.clear();
auto player_pos = world.get<Position>(level.player).location;
Point cam_orig = map.center_camera(player_pos, view_x, view_y);
auto &tiles = map.tiles();
auto &tile_set = textures::get_map_tile_set();
/* I'm doing double tid->wchar_t conversion here, maybe just
* render the tids into the grid then let someone else do this. */
// first fill it with the map cells
for(shiterator::each_cell_t it{grid}; it.next();) {
size_t tile_y = size_t(it.y) + cam_orig.y;
size_t tile_x = size_t(it.x) + cam_orig.x;
if(matrix::inbounds(tiles, tile_x, tile_y) && fow[tile_y][tile_x]) {
size_t tid = tiles[tile_y][tile_x];
grid[it.y][it.x] = tile_set[tid];
} else {
grid[it.y][it.x] = L' ';
}
}
// then get the enemy/item/device tiles and fill those in
world.query<Position, Tile>([&](auto, auto &pos, auto &entity_glyph) {
// BUG: don't I have a within bounds macro somewhere?
if(pos.location.x >= cam_orig.x
&& pos.location.x <= cam_orig.x + view_x
&& pos.location.y >= cam_orig.y
&& pos.location.y <= cam_orig.y + view_y)
{
if(fow[pos.location.y][pos.location.x]) {
Point view_pos = map.map_to_camera(pos.location, cam_orig);
entity_map.insert_or_assign(view_pos, entity_glyph.display);
}
}
});
}
void System::render_map(Matrix& tiles, EntityGrid& entity_map, sf::RenderTexture& render, int compass_dir, wchar_t player_display) {
sf::Vector2i tile_sprite_dim{MAP_TILE_DIM,MAP_TILE_DIM};
unsigned int width = matrix::width(tiles);
unsigned int height = matrix::height(tiles);
sf::Vector2u dim{width * tile_sprite_dim.x, height * tile_sprite_dim.y};
auto render_size = render.getSize();
if(render_size.x != width || render_size.y != height) {
bool worked = render.resize(dim);
dbc::check(worked, "Failed to resize map render target.");
}
render.clear({0,0,0,255});
for(matrix::each_row it{tiles}; it.next();) {
wchar_t display = tiles[it.y][it.x];
if(display == L' ') continue; // skip for now
auto& sprite = textures::get_map_sprite(display);
sprite.setPosition({float(it.x * tile_sprite_dim.x), float(it.y * tile_sprite_dim.y)});
render.draw(sprite);
}
for(auto [point, display] : entity_map) {
auto& sprite = textures::get_map_sprite(display);
if(display == player_display) {
sf::Vector2f center{float(tile_sprite_dim.x / 2), float(tile_sprite_dim.y / 2)};
float degrees = (((compass_dir * 45) + PLAYER_SPRITE_DIR_CORRECTION) % 360);
sprite.setOrigin(center);
sprite.setRotation(sf::degrees(degrees));
sprite.setPosition({float(point.x * tile_sprite_dim.x) + center.x, float(point.y * tile_sprite_dim.y) + center.y});
} else {
sprite.setPosition({float(point.x * tile_sprite_dim.x), float(point.y * tile_sprite_dim.y)});
}
render.draw(sprite);
}
render.display();
}
bool System::use_item(const string& slot_name) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& inventory = world.get<inventory::Model>(level.player);
auto& player_combat = world.get<Combat>(level.player);
if(player_combat.hp >= player_combat.max_hp) return false;
if(!inventory.has(slot_name)) return false;
auto what = inventory.get(slot_name);
if(auto curative = world.get_if<Curative>(what)) {
inventory.remove(what);
player_combat.hp += curative->hp;
if(player_combat.hp > player_combat.max_hp) {
player_combat.hp = player_combat.max_hp;
}
dbc::log($F("player health now {}",
player_combat.hp));
world.remove<Curative>(what);
return true;
} else {
dbc::log($F("no usable item at {}", what));
return false;
}
}
game::Event System::shortest_rotate(Point player_at, Point aiming_at, Point target) {
dbc::check(aiming_at != target, "you're already pointing there.");
dbc::check(player_at != target, "you can't turn on yourself");
float target_dx = float(player_at.x) - float(target.x);
float target_dy = float(player_at.y) - float(target.y);
float aiming_dx = float(player_at.x) - float(aiming_at.x);
float aiming_dy = float(player_at.y) - float(aiming_at.y);
float target_angle = atan2(-target_dy, target_dx) * (180.0 / std::numbers::pi);
float aiming_angle = atan2(-aiming_dy, aiming_dx) * (180.0 / std::numbers::pi);
float diff = target_angle - aiming_angle;
double normalized = fmod(diff + 360.0, 360.0);
return normalized < 180.0 ? game::Event::ROTATE_LEFT : game::Event::ROTATE_RIGHT;
}
void System::clear_attack() {
auto world = GameDB::current_world();
std::vector<Entity> dead_anim;
world->query<animation::Animation, Temporary>([&](auto ent, auto& anim, auto&) {
if(!anim.playing) dead_anim.push_back(ent);
});
for(auto ent : dead_anim) {
world->remove<Sprite>(ent);
world->remove<animation::Animation>(ent);
world->remove<SpriteEffect>(ent);
remove_from_world(ent);
}
}
void System::spawn_attack(World& world, int attack_id, DinkyECS::Entity enemy) {
}

74
src/game/systems.hpp Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include "game/components.hpp"
#include <SFML/Graphics/RenderTexture.hpp>
#include "game/map.hpp"
#include "algos/spatialmap.hpp"
#include "game/level.hpp"
#include "events.hpp"
namespace System {
using namespace components;
using namespace DinkyECS;
using std::string, matrix::Matrix;
void lighting();
void motion();
void collision();
void death();
void generate_paths();
void enemy_pathing();
void enemy_ai_initialize();
void device(World &world, Entity actor, Entity item);
void move_player(Position move_to);
Entity spawn_item(World& world, const string& name);
void drop_item(Entity item);
void enemy_ai();
void combat(int attack_id);
std::shared_ptr<sf::Shader> sprite_effect(Entity entity);
void player_status();
void distribute_loot(Position target_pos);
void pickup();
bool place_in_container(Entity cont_id, const string& name, Entity world_entity);
void remove_from_container(Entity cont_id, const std::string& name);
void remove_from_world(Entity entity);
void inventory_swap(Entity container_id, const std::string& a_name, const std::string &b_name);
bool inventory_occupied(Entity container_id, const std::string& name);
void draw_map(Matrix& grid, EntityGrid& entity_map);
void render_map(Matrix& tiles, EntityGrid& entity_map, sf::RenderTexture& render, int compass_dir, wchar_t player_display);
void set_position(DinkyECS::World& world, SpatialMap& collision, Entity entity, Position pos);
bool use_item(const std::string& slot_name);
game::Event shortest_rotate(Point player_at, Point aiming_at, Point turning_to);
template <typename T>
void multi_path(GameDB::Level& level, Pathing& paths, Matrix& walls) {
// first, put everything of this type as a target
level.world->query<Position, T>(
[&](const auto ent, auto& position, auto&) {
if(ent != level.player) {
paths.set_target(position.location);
}
});
level.world->query<Collision>(
[&](const auto ent, auto& collision) {
if(collision.has && ent != level.player) {
auto& pos = level.world->get<Position>(ent);
walls[pos.location.y][pos.location.x] = WALL_VALUE;
}
});
paths.compute_paths(walls);
}
void clear_attack();
void spawn_attack(World& world, int attack_id, DinkyECS::Entity enemy);
}

256
src/game/worldbuilder.cpp Normal file
View file

@ -0,0 +1,256 @@
#include "game/worldbuilder.hpp"
#include "algos/rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "game/components.hpp"
#include "algos/maze.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/systems.hpp"
#include "graphics/animation.hpp"
using namespace fmt;
using namespace components;
void WorldBuilder::stylize_rooms() {
auto& tiles = $map.tiles();
auto style_config = settings::get("room_themes");
json& styles = style_config.json();
for(auto& room : $map.rooms()) {
auto& style = styles[Random::uniform(size_t(0), styles.size() - 1)];
dbc::check(style.contains("floor"),
$F("no floor spec in style {}", (std::string)style["name"]));
dbc::check(style.contains("walls"),
$F("no walls spec in style {}", (std::string)style["name"]));
auto& floor_name = style["floor"];
auto& wall_name = style["walls"];
size_t floor_id = textures::get_id(floor_name);
size_t wall_id = textures::get_id(wall_name);
for(matrix::box it{tiles, room.x, room.y, room.width+1, room.height+1}; it.next();) {
if(tiles[it.y][it.x] == 1) {
tiles[it.y][it.x] = wall_id;
} else if(tiles[it.y][it.x] == 0) {
tiles[it.y][it.x] = floor_id;
}
}
}
}
void WorldBuilder::generate_map() {
auto script = R"(
[
{"action": "hunt_and_kill"},
{"action": "clear"},
{"action": "randomize_rooms", "data": [3]},
{"action": "hunt_and_kill"},
{"action": "place_doors"}
]
)"_json;
int i = 0;
for(; i < 10; i++) {
auto [maze, valid] = maze::script($map, script);
if(valid) {
break;
} else {
maze.dump(fmt::format("FAILED width={}", $map.width()), true);
}
}
dbc::check(i < 10, "failed to find a valid map after 10 attempts");
$map.init_tiles();
stylize_rooms();
}
bool WorldBuilder::find_open_spot(Point& pos_out) {
size_t i = 0;
// horribly bad but I need to place things _somewhere_ so just fan out
for(i = 2; i < $map.width(); i++) {
// rando_rect starts at the top/left corner not center
for(matrix::rando_box it{$map.walls(), pos_out.x, pos_out.y, i}; it.next();) {
Point test{size_t(it.x), size_t(it.y)};
if($map.can_move(test) && !$collision.something_there(test)) {
pos_out = test;
return true;
}
}
}
matrix::dump("FAIL PLACE!", $map.walls(), pos_out.x, pos_out.y);
dbc::sentinel($F("failed to place entity in the entire map?: i={}; width={};", i, $map.width()));
return false;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_map(DinkyECS::World &world, json &entity_data, Point pos) {
bool found = find_open_spot(pos);
dbc::check(found, "Failed to find a place for this thing.");
auto item = world.entity();
int inv_count = entity_data.contains("inventory_count") ? (int)entity_data["inventory_count"] : 0;
if(inv_count > 0) {
world.set<InventoryItem>(item, {entity_data["inventory_count"], entity_data});
}
if(entity_data.contains("components")) {
components::configure_entity(world, item, entity_data["components"]);
}
System::set_position(world, $collision, item, {pos.x, pos.y});
animation::configure(world, item);
return item;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_room(DinkyECS::World &world, json &entity_data, int in_room) {
Point pos_out;
bool placed = $map.place_entity(in_room, pos_out);
dbc::check(placed, "failed to randomly place item in room");
auto entity = configure_entity_in_map(world, entity_data, pos_out);
return entity;
}
inline json &select_entity_type(GameConfig &config, json &gen_config) {
int enemy_test = Random::uniform<int>(0,100);
int device_test = Random::uniform<int>(0, 100);
if(enemy_test < gen_config["enemy_probability"]) {
return config.enemies.json();
} else if(device_test < gen_config["device_probability"]) {
return config.devices.json();
} else {
return config.items.json();
}
}
inline json& random_entity_data(GameConfig& config, json& gen_config) {
json& entity_db = select_entity_type(config, gen_config);
std::vector<std::string> keys;
for(auto& el : entity_db.items()) {
auto& data = el.value();
if(data["placement"] == nullptr) {
keys.push_back(el.key());
}
}
int rand_entity = Random::uniform<int>(0, keys.size() - 1);
std::string key = keys[rand_entity];
return entity_db[key];
}
void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) {
auto& gen_config = config.game["worldgen"];
for(int room_num = $map.room_count() - 1; room_num > 0; room_num--) {
// pass that to the config as it'll be a generic json
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_room(world, entity_data, room_num);
}
for(auto& at : $map.$dead_ends) {
if($map.$doors.contains(at)) continue;
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_map(world, entity_data, at);
}
}
void WorldBuilder::place_doors(DinkyECS::World& world, GameConfig& config) {
auto& device_config = config.devices.json();
auto entity_data = device_config["DOOR_PLAIN"];
auto& tiles = $map.tiles();
auto& walls = $map.walls();
for(auto [door_at, _] : $map.$doors) {
// note, we set this to WALL_VALUE so it renders as a wall but map.iswall will check if its a door for collision
walls[door_at.y][door_at.x] = WALL_VALUE;
for(matrix::compass it{tiles, door_at.x, door_at.y}; it.next();) {
if(walls[it.y][it.x] == WALL_VALUE) {
// found a wall near the door, and since doors always have n/s/e/w walls it should be the one to use
size_t wall_id = tiles[it.y][it.x]; // this is wall to use
tiles[door_at.y][door_at.x] = textures::door_for_wall(wall_id);
break;
}
}
}
}
void WorldBuilder::place_stairs(DinkyECS::World& world, GameConfig& config) {
auto& device_config = config.devices.json();
auto entity_data = device_config["STAIRS_DOWN"];
auto at_end = $map.$dead_ends.back();
configure_entity_in_map(world, entity_data, at_end);
}
void WorldBuilder::configure_starting_items(DinkyECS::World &world) {
auto& player = world.get_the<Player>();
auto torch_id = System::spawn_item(world, "TORCH_BAD");
auto &inventory = world.get<inventory::Model>(player.entity);
inventory.add("hand_r", torch_id);
world.make_constant(torch_id);
auto healing = System::spawn_item(world, "POTION_HEALING_SMALL");
inventory.add("pocket_l", healing);
world.make_constant(healing);
}
void WorldBuilder::place_entities(DinkyECS::World &world) {
auto &config = world.get_the<GameConfig>();
// configure a player as a fact of the world
Position player_pos{0,0};
if(world.has_the<Player>()) {
auto& player = world.get_the<Player>();
// first get a guess from the map
bool placed = $map.place_entity(0, player_pos.location);
dbc::check(placed, "map.place_entity failed to position player");
// then use the collision map to place the player safely
placed = find_open_spot(player_pos.location);
dbc::check(placed, "WorldBuild.find_open_spot also failed to position player");
System::set_position(world, $collision, player.entity, player_pos);
} else {
auto player_data = config.enemies["PLAYER_TILE"];
auto player_ent = configure_entity_in_room(world, player_data, 0);
player_pos = world.get<Position>(player_ent);
// configure player in the world
Player player{player_ent};
world.set_the<Player>(player);
world.set<inventory::Model>(player_ent, {});
configure_starting_items(world);
world.make_constant(player.entity);
}
dbc::check(player_pos.location.x != 0 && player_pos.location.y != 0,
"failed to place the player correctly");
place_doors(world, config);
randomize_entities(world, config);
place_stairs(world, config);
}
void WorldBuilder::generate(DinkyECS::World &world) {
generate_map();
place_entities(world);
}

32
src/game/worldbuilder.hpp Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include "game/map.hpp"
#include "algos/dinkyecs.hpp"
#include "game/components.hpp"
#include "algos/spatialmap.hpp"
class WorldBuilder {
public:
Map& $map;
SpatialMap& $collision;
WorldBuilder(Map &map, SpatialMap& collision) :
$map(map),
$collision(collision)
{ }
void generate_map();
DinkyECS::Entity configure_entity_in_map(DinkyECS::World &world, nlohmann::json &entity_data, Point pos);
DinkyECS::Entity configure_entity_in_room(DinkyECS::World &world, nlohmann::json &entity_data, int in_room);
bool find_open_spot(Point& pos_out);
void place_entities(DinkyECS::World &world);
void generate(DinkyECS::World &world);
void randomize_entities(DinkyECS::World &world, components::GameConfig &config);
void place_stairs(DinkyECS::World& world, components::GameConfig& config);
void place_doors(DinkyECS::World& world, components::GameConfig& config);
void configure_starting_items(DinkyECS::World &world);
void stylize_rooms();
};

312
src/graphics/animation.cpp Normal file
View file

@ -0,0 +1,312 @@
#include "graphics/animation.hpp"
#include <memory>
#include <chrono>
#include "dbc.hpp"
#include "algos/rand.hpp"
#include <iostream>
#include <fstream>
#include "game/sound.hpp"
#include "game/components.hpp"
constexpr float SUB_FRAME_SENSITIVITY = 0.999f;
namespace animation {
using namespace std::chrono_literals;
std::vector<sf::IntRect> Animation::calc_frames() {
dbc::check(sequence.frames.size() == sequence.durations.size(), "sequence.frames.size() != sequence.durations.size()");
std::vector<sf::IntRect> frames;
for(int frame_i : sequence.frames) {
dbc::check(frame_i < sheet.frames, "frame index greater than sheet frames");
frames.emplace_back(
sf::Vector2i{sheet.frame_width * frame_i, 0}, // NOTE: one row only for now
sf::Vector2i{sheet.frame_width,
sheet.frame_height});
}
return frames;
}
void Animation::play() {
dbc::check(!playing, "can't call play while playing?");
sequence.current = 0;
sequence.subframe = 0.0f;
sequence.loop_count = 0;
playing = true;
sequence.timer.start();
sequence.INVARIANT();
}
void Animation::stop() {
playing = false;
sequence.timer.reset();
}
// need one for each kind of thing to animate
// NOTE: possibly find a way to only run apply on frame change?
void Animation::apply(sf::Sprite& sprite) {
dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
// NOTE: pos is not updated yet
auto& rect = $frame_rects.at(sequence.current);
sprite.setTextureRect(rect);
}
/*
* Alternative mostly used in raycaster.cpp that
* DOES NOT setTextureRect() but just points
* the rect_io at the correct frame, but leaves
* it's size and base position alone.
*/
void Animation::apply(sf::Sprite& sprite, sf::IntRect& rect_io) {
dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
auto& rect = $frame_rects.at(sequence.current);
rect_io.position.x += rect.position.x;
rect_io.position.y += rect.position.y;
}
void Animation::motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale) {
sequence.INVARIANT();
transform.apply(sequence, pos, scale);
if(transform.flipped) {
scale.x *= -1;
}
sprite.setPosition(pos);
if(transform.scaled) {
sprite.setScale(scale);
}
}
void Animation::motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f size) {
dbc::check(size.x > 1.0f && size.y > 1.0f, "motion size must be above 1.0 since it's not a ratio");
dbc::check(transform.flipped == false, "transform must be false, has no effect on View");
sf::Vector2f scale{transform.min_x, transform.min_y};
transform.apply(sequence, pos, scale);
view_out.setCenter(pos);
if(transform.scaled) {
view_out.setSize({size.x * scale.x, size.y * scale.y});
} else {
view_out.setSize(size);
}
}
void Animation::apply_effect(std::shared_ptr<sf::Shader> effect) {
dbc::check(effect != nullptr, "can't apply null effect");
effect->setUniform("u_time", sequence.timer.getElapsedTime().asSeconds());
sf::Vector2f u_resolution{float(sheet.frame_width), float(sheet.frame_height)};
effect->setUniform("u_resolution", u_resolution);
}
void Animation::play_sound() {
// BUG: this can be optimized way better
if(sounds.contains(form_name)) {
for(auto& [at_frame, sound_name] : sounds.at(form_name)) {
if(sequence.current == at_frame) {
sound::play(sound_name);
}
}
} else {
fmt::println("Animation has not sound {}", form_name);
}
}
/* REFACTOR: I believe this is wrong still. If ::commit() determines number of ticks+alpha since last
* render then update needs to be called 1/tick. The Timer will keep track of alpha as the error
* between commit calls, so this function only really needs to care about ticks. But, I'm still
* calling getElapsedTime() when I already did that in commit(), so should I just ignore that and assume
* elapsed is DELTA, or use elapsed here?
*/
void Animation::update() {
dbc::check(playing, "attempt to update animation that's not playing");
sequence.INVARIANT();
auto [ticks, alpha] = sequence.timer.commit();
int duration = sequence.durations.at(sequence.current);
sequence.subframe += ticks;
sequence.easing_position += ticks;
bool frame_change = false;
if(sequence.subframe >= duration) {
sequence.timer.restart();
sequence.current++;
sequence.subframe = 0;
frame_change = true;
}
if(sequence.current >= sequence.frame_count) {
sequence.loop_count++;
sequence.easing_position = 0;
playing = onLoop(sequence, transform);
sequence.INVARIANT();
}
if(frame_change) play_sound();
if(frame_change && onFrame != nullptr) onFrame();
}
void Timer::start() {
clock.start();
prev_time = clock.getElapsedTime().asSeconds();
}
void Timer::reset() {
elapsed_ticks = 0;
clock.reset();
}
void Timer::restart() {
elapsed_ticks = 0;
clock.restart();
prev_time = clock.getElapsedTime().asSeconds();
}
sf::Time Timer::getElapsedTime() {
return clock.getElapsedTime();
}
std::pair<int, double> Timer::commit() {
// determine frame duration based on previous time
current_time = clock.getElapsedTime().asSeconds();
frame_duration = current_time - prev_time;
// update prev_time for the next call
prev_time = current_time;
// update accumulator, retaining previous errors
accumulator += frame_duration;
// find the tick count based on DELTA
double tick_count = floor(accumulator / DELTA);
// reduce accumulator by the number of DELTAS
accumulator -= tick_count * DELTA;
// that leaves the remaining errors for next loop
elapsed_ticks += tick_count;
// alpha is then what we lerp...but WHY?!
alpha = accumulator / DELTA;
// return the number of even DELTA ticks and the alpha
return {int(tick_count), alpha};
}
void Transform::apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out) {
float tick = easing_func(seq.easing_position / seq.easing_duration);
motion_func(*this, pos_out, scale_out, tick, relative);
}
bool Animation::has_form(const std::string& as_form) {
return forms.contains(as_form);
}
void Animation::set_form(const std::string& as_form) {
dbc::check(forms.contains(as_form),
$F("form {} does not exist in animation", as_form));
stop();
const auto& [seq_name, tr_name] = forms.at(as_form);
dbc::check(sequences.contains(seq_name),
$F("sequences do NOT have \"{}\" name", seq_name));
dbc::check(transforms.contains(tr_name),
$F("transforms do NOT have \"{}\" name", tr_name));
// everything good, do the update
form_name = as_form;
sequence_name = seq_name;
transform_name = tr_name;
sequence = sequences.at(seq_name);
transform = transforms.at(tr_name);
sequence.frame_count = sequence.frames.size();
// BUG: should this be configurable instead?
for(auto duration : sequence.durations) {
sequence.easing_duration += float(duration);
}
dbc::check(sequence.easing_duration > 0.0, "bad easing duration");
$frame_rects = calc_frames();
transform.easing_func = ease2::get_easing(transform.easing);
transform.motion_func = ease2::get_motion(transform.motion);
sequence.INVARIANT();
}
Animation load(const std::string &file, const std::string &anim_name) {
using nlohmann::json;
std::ifstream infile(file);
auto data = json::parse(infile);
dbc::check(data.contains(anim_name),
$F("{} animation config does not have animation {}", file, anim_name));
Animation anim;
animation::from_json(data[anim_name], anim);
anim.name = anim_name;
dbc::check(anim.forms.contains("idle"),
$F("animation {} must have 'idle' form", anim_name));
anim.set_form("idle");
return anim;
}
void Sequence::INVARIANT(const std::source_location location) {
dbc::check(frames.size() == durations.size(),
$F("frames.size={} doesn't match durations.size={}",
frames.size(), durations.size()), location);
dbc::check(easing_duration > 0.0, $F("bad easing duration: {}", easing_duration), location);
dbc::check(frame_count == frames.size(),
$F("frame_count={} doesn't match frames.size={}", frame_count, frames.size()), location);
dbc::check(frame_count == durations.size(),
$F("frame_count={} doesn't match durations.size={}", frame_count, durations.size()), location);
dbc::check(current < durations.size(),
$F("current={} went past end of fame durations.size={}", current, durations.size()), location);
}
// BUG: BAAADD REMOVE
bool has(const std::string& name) {
using nlohmann::json;
std::ifstream infile("assets/animation.json");
auto data = json::parse(infile);
return data.contains(name);
}
void configure(DinkyECS::World& world, DinkyECS::Entity entity) {
auto sprite = world.get_if<components::Sprite>(entity);
if(sprite != nullptr && has(sprite->name)) {
world.set<Animation>(entity, animation::load("assets/animation.json", sprite->name));
}
}
void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity) {
auto anim = world.get_if<Animation>(entity);
if(anim != nullptr && !anim->playing) {
anim->play();
}
}
}

150
src/graphics/animation.hpp Normal file
View file

@ -0,0 +1,150 @@
#pragma once
#include <memory>
#include <chrono>
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/System/Time.hpp>
#include <functional>
#include "graphics/easing.hpp"
#include <fmt/core.h>
#include "game/json_mods.hpp"
#include <source_location>
#include "algos/dinkyecs.hpp"
namespace animation {
template <typename T> struct NameOf;
struct Sheet {
int frames{0};
int frame_width{0};
int frame_height{0};
};
struct Timer {
double DELTA = 1.0/60.0;
double accumulator = 0.0;
double prev_time = 0.0;
double current_time = 0.0;
double frame_duration = 0.0;
double alpha = 0.0;
int elapsed_ticks = 0;
sf::Clock clock{};
std::pair<int, double> commit();
void start();
void reset();
void restart();
sf::Time getElapsedTime();
};
struct Sequence {
std::vector<int> frames{};
std::vector<int> durations{}; // in ticks
size_t current{0};
int loop_count{0};
size_t frame_count{frames.size()};
Timer timer{};
int subframe{0};
float easing_duration{0.0f};
float easing_position{0.0f};
void INVARIANT(const std::source_location location = std::source_location::current());
};
struct Transform {
// how to know when a transform ends?
float min_x{1.0f};
float min_y{1.0f};
float max_x{1.0f};
float max_y{1.0f};
bool flipped{false};
bool scaled{false};
bool relative{false};
// handled by onLoop
bool toggled{false};
bool looped{false};
std::string easing{"in_out_back"};
std::string motion{"move_rush"};
// change to using a callback function for these
ease2::EaseFunc easing_func{ease2::get_easing(easing)};
ease2::MotionFunc motion_func{ease2::get_motion(motion)};
std::shared_ptr<sf::Shader> shader{nullptr};
void apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out);
};
/* Gets the number of times it looped, and returns if it should stop. */
using OnLoopHandler = std::function<bool(Sequence& seq, Transform& tr)>;
using OnFrameHandler = std::function<void()>;
inline bool DefaultOnLoop(Sequence& seq, Transform& tr) {
if(tr.toggled) {
seq.current = seq.frame_count - 1;
} else {
seq.current = 0;
}
return tr.looped;
}
using Form = std::pair<std::string, std::string>;
using Sound = std::pair<size_t, std::string>;
class Animation {
public:
Sheet sheet;
std::unordered_map<std::string, Sequence> sequences;
std::unordered_map<std::string, Transform> transforms;
std::unordered_map<std::string, Form> forms;
std::unordered_map<std::string, std::vector<Sound>> sounds;
OnFrameHandler onFrame = nullptr;
Sequence sequence{};
Transform transform{};
std::vector<sf::IntRect> $frame_rects{calc_frames()};
OnLoopHandler onLoop = DefaultOnLoop;
bool playing = false;
// mostly for debugging purposes
std::string form_name="idle";
std::string sequence_name="";
std::string transform_name="";
std::string name="";
std::vector<sf::IntRect> calc_frames();
void play();
void play_sound();
void stop();
bool has_form(const std::string& as_form);
void set_form(const std::string& form);
void apply(sf::Sprite& sprite);
void apply(sf::Sprite& sprite, sf::IntRect& rect_io);
void apply_effect(std::shared_ptr<sf::Shader> effect);
void update();
void motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale);
void motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f scale);
};
Animation load(const std::string &file, const std::string &anim_name);
// BUG: brought over from animation to finish the refactor, but these may not be needed or maybe they go in system.cpp?
bool has(const std::string& name);
void configure(DinkyECS::World& world, DinkyECS::Entity entity);
void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity);
ENROLL_COMPONENT(Sheet, frames, frame_width, frame_height);
ENROLL_COMPONENT(Sequence, frames, durations);
ENROLL_COMPONENT(Transform, min_x, min_y, max_x, max_y,
flipped, scaled, relative, toggled, looped, easing, motion);
ENROLL_COMPONENT(Animation, sheet, sequences, transforms, forms, sounds);
}

132
src/graphics/camera.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "graphics/camera.hpp"
#include <unordered_map>
#include "game/components.hpp"
#include "game/config.hpp"
#include <algorithm>
#include <iostream>
#include <cstdlib>
namespace cinematic {
using animation::Animation, std::string, std::min, std::clamp;
struct CameraManager {
std::unordered_map<string, Animation> animations;
};
static CameraManager MGR;
static bool initialized = false;
void init() {
if(!initialized) {
// BUG: it should be that you give a camera to load by name, not just one for all cameras
auto data = settings::get("cameras");
for(auto [key, value] : data.json().items()) {
auto anim = components::convert<Animation>(value);
MGR.animations.try_emplace(key, anim);
}
initialized = true;
}
}
Camera::Camera(sf::Vector2f size, const std::string &name) :
anim(MGR.animations.at(name)),
size(size),
base_size(size),
aimed_at{size.x/2, size.y/2},
going_to{size.x/2, size.y/2},
camera_bounds{{0,0}, size},
view{aimed_at, size}
{
anim.sheet.frame_width = base_size.x;
anim.sheet.frame_height = base_size.y;
}
void Camera::update_camera_bounds(sf::Vector2f size) {
// camera bounds now constrains the x/y so that the mid-point
// of the size won't go too far outside of the frame
camera_bounds = {
{size.x / 2.0f, size.y / 2.0f},
{base_size.x - size.x / 2.0f, base_size.y - size.y / 2.0f}
};
}
void Camera::scale(float ratio) {
size.x = base_size.x * ratio;
size.y = base_size.y * ratio;
update_camera_bounds(size);
}
void Camera::resize(float width) {
dbc::check(width <= base_size.x, "invalid width for camera");
size.x = width;
size.y = base_size.y * (width / base_size.x);
update_camera_bounds(size);
}
void Camera::style(const std::string &name) {
anim.set_form(name);
}
void Camera::position(float x, float y) {
aimed_at.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
aimed_at.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);
}
void Camera::move(float x, float y) {
going_to.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
going_to.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);
if(!anim.transform.relative) {
anim.transform.min_x = aimed_at.x;
anim.transform.min_y = aimed_at.y;
anim.transform.max_x = going_to.x;
anim.transform.max_y = going_to.y;
}
}
void Camera::reset(sf::RenderTexture& target) {
size = {base_size.x, base_size.y};
aimed_at = {base_size.x/2, base_size.y/2};
going_to = {base_size.x/2, base_size.y/2};
view = {aimed_at, size};
camera_bounds = {{0,0}, base_size};
// BUG: is getDefaultView different from view?
target.setView(target.getDefaultView());
}
void Camera::render(sf::RenderTexture& target) {
if(anim.playing) {
anim.motion(view, going_to, size);
target.setView(view);
}
}
void Camera::update() {
if(anim.playing) anim.update();
}
bool Camera::playing() {
return anim.playing;
}
void Camera::play() {
anim.play();
}
void Camera::from_story(components::Storyboard& story) {
anim.sequences.clear();
anim.forms.clear();
for(auto& [timecode, cell, transform, duration] : story.beats) {
animation::Sequence seq{.frames={0}, .durations={std::stoi(duration)}};
anim.sequences.try_emplace(timecode, seq);
animation::Form form{timecode, transform};
anim.forms.try_emplace(timecode, form);
}
}
}

37
src/graphics/camera.hpp Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "graphics/animation.hpp"
#include "constants.hpp"
#include <SFML/Graphics/RenderTexture.hpp>
namespace components {
struct Storyboard;
}
namespace cinematic {
struct Camera {
animation::Animation anim;
sf::Vector2f size{SCREEN_WIDTH, SCREEN_HEIGHT};
sf::Vector2f base_size{SCREEN_WIDTH, SCREEN_HEIGHT};
sf::Vector2f aimed_at{0,0};
sf::Vector2f going_to{0,0};
sf::FloatRect camera_bounds{{0,0},{SCREEN_WIDTH, SCREEN_HEIGHT}};
sf::View view;
Camera(sf::Vector2f size, const std::string &name);
void resize(float width);
void scale(float ratio);
void position(float x, float y);
void move(float x, float y);
bool playing();
void update();
void render(sf::RenderTexture& target);
void play();
void style(const std::string &name);
void reset(sf::RenderTexture& target);
void update_camera_bounds(sf::Vector2f size);
void from_story(components::Storyboard& story);
};
void init();
}

141
src/graphics/easing.cpp Normal file
View file

@ -0,0 +1,141 @@
#include "algos/rand.hpp"
#include "graphics/animation.hpp"
#include <fmt/core.h>
#include <unordered_map>
#include "dbc.hpp"
namespace ease2 {
using namespace animation;
double none(float tick) {
return 0.0;
}
double linear(float tick) {
return tick;
}
double sine(double x) {
// old one? return std::abs(std::sin(seq.subframe * ease_rate));
return (std::sin(x) + 1.0) / 2.0;
}
double out_circle(double x) {
return std::sqrt(1.0f - ((x - 1.0f) * (x - 1.0f)));
}
double out_bounce(double x) {
constexpr const double n1 = 7.5625;
constexpr const double d1 = 2.75;
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
x -= 1.5;
return n1 * (x / d1) * x + 0.75;
} else if (x < 2.5 / d1) {
x -= 2.25;
return n1 * (x / d1) * x + 0.9375;
} else {
x -= 2.625;
return n1 * (x / d1) * x + 0.984375;
}
}
double in_out_back(double x) {
constexpr const double c1 = 1.70158;
constexpr const double c2 = c1 * 1.525;
return x < 0.5
? (std::pow(2.0 * x, 2.0) * ((c2 + 1.0) * 2.0 * x - c2)) / 2.0
: (std::pow(2.0 * x - 2.0, 2.0) * ((c2 + 1.0) * (x * 2.0 - 2.0) + c2) + 2.0) / 2.0;
}
double random(double tick) {
return Random::uniform_real(0.0001f, 1.0f);
}
double normal_dist(double tick) {
return Random::normal(0.5f, 0.1f);
}
void move_shake(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (pos_out.x * relative);
}
void move_bounce(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (pos_out.y * relative);
}
void move_rush(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y) + (pos_out.y * relative);
}
void scale_squeeze(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
}
void scale_squash(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
}
void scale_stretch(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
}
void scale_grow(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
}
void move_slide(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (pos_out.x * relative);
pos_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (pos_out.y * relative);
}
void move_none(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
}
void scale_both(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(scale_out.x * tr.min_x, scale_out.x * tr.max_x, tick) + (scale_out.x * relative);
scale_out.y = std::lerp(scale_out.y * tr.min_y, scale_out.y * tr.max_y, tick) + (scale_out.y * relative);
}
std::unordered_map<std::string, EaseFunc> map_of_easings{
{"sine", sine},
{"out_circle", out_circle},
{"out_bounce", out_bounce},
{"in_out_back", in_out_back},
{"random", random},
{"normal_dist", normal_dist},
{"none", none},
{"linear", linear},
};
std::unordered_map<std::string, MotionFunc> map_of_motions{
{"move_bounce", move_bounce},
{"move_rush", move_rush},
{"scale_squeeze", scale_squeeze},
{"scale_squash", scale_squash},
{"scale_stretch", scale_stretch},
{"scale_grow", scale_grow},
{"move_slide", move_slide},
{"move_none", move_none},
{"scale_both", scale_both},
{"move_shake", move_shake},
};
EaseFunc get_easing(const std::string& name) {
dbc::check(map_of_easings.contains(name),
$F("easing name {} does not exist", name));
return map_of_easings.at(name);
}
MotionFunc get_motion(const std::string& name) {
dbc::check(map_of_motions.contains(name),
$F("motion name {} does not exist", name));
return map_of_motions.at(name);
}
}

32
src/graphics/easing.hpp Normal file
View file

@ -0,0 +1,32 @@
#include <functional>
#include "graphics/animation.hpp"
namespace animation {
struct Transform;
}
namespace ease2 {
using EaseFunc = std::function<double(double)>;
using MotionFunc = std::function<void(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative)>;
EaseFunc get_easing(const std::string& name);
MotionFunc get_motion(const std::string& name);
double sine(double x);
double out_circle(double x);
double out_bounce(double x);
double in_out_back(double x);
double random(double tick);
double normal_dist(double tick);
void move_bounce(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_rush(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_squeeze(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_squash(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_stretch(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_grow(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_slide(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_none(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_both(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_shake(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
}

87
src/graphics/lights.cpp Normal file
View file

@ -0,0 +1,87 @@
#include "graphics/lights.hpp"
#include "constants.hpp"
#include "graphics/textures.hpp"
#include <vector>
using std::vector;
namespace lighting {
LightRender::LightRender(Matrix& tiles) :
$width(matrix::width(tiles)),
$height(matrix::height(tiles)),
$lightmap(matrix::make($width, $height)),
$ambient(matrix::make($width, $height)),
$paths($width, $height),
$fow(matrix::make($width, $height))
{
auto& tile_ambient = textures::get_ambient_light();
for(matrix::each_cell it{tiles}; it.next();) {
size_t tile_id = tiles[it.y][it.x];
$ambient[it.y][it.x] = MIN + tile_ambient[tile_id];
}
}
void LightRender::render_square_light(LightSource source, Point at, PointList &has_light) {
for(matrix::box it{$lightmap, at.x, at.y, (size_t)floor(source.radius)}; it.next();) {
if($paths.$paths[it.y][it.x] != WALL_PATH_LIMIT) {
$lightmap[it.y][it.x] = light_level(source.strength, it.distance(), it.x, it.y);
has_light.emplace_back(it.x, it.y);
}
}
}
/*
* NOTE: This really doesn't need to calculate light all the time. It doesn't
* change around the light source until the lightsource is changed, so the
* light levels could be placed in a Matrix inside LightSource, calculated once
* and then simply "applied" to the area where the entity is located. The only
* thing that would need to be calculated each time is the walls.
*/
void LightRender::render_light(LightSource source, Point at) {
clear_light_target(at);
PointList has_light;
render_square_light(source, at, has_light);
for(auto point : has_light) {
for(matrix::compass it{$lightmap, point.x, point.y}; it.next();) {
if($paths.$paths[it.y][it.x] == WALL_PATH_LIMIT) {
$lightmap[it.y][it.x] = light_level(source.strength, 1.5f, point.x, point.y);
}
}
}
}
int LightRender::light_level(int strength, float distance, size_t x, size_t y) {
int boosted = strength + BOOST;
int new_level = distance <= 1.0f ? boosted : boosted / sqrt(distance);
int cur_level = $lightmap[y][x];
return cur_level < new_level ? new_level : cur_level;
}
void LightRender::reset_light() {
$lightmap = $ambient;
}
void LightRender::clear_light_target(const Point &at) {
$paths.clear_target(at);
}
void LightRender::set_light_target(const Point &at, int value) {
$paths.set_target(at, value);
}
void LightRender::update_fow(Point at, LightSource source) {
for(matrix::circle it{$lightmap, at, source.radius}; it.next();) {
for(auto x = it.left; x < it.right; x++) {
$fow[it.y][x] = $lightmap[it.y][x];
}
}
}
void LightRender::path_light(Matrix &walls) {
$paths.compute_paths(walls);
}
}

40
src/graphics/lights.hpp Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <array>
#include "dbc.hpp"
#include "algos/point.hpp"
#include <algorithm>
#include "algos/matrix.hpp"
#include "algos/pathing.hpp"
#include "game/components.hpp"
namespace lighting {
using components::LightSource;
// THESE ARE PERCENTAGES!
const int MIN = 20;
const int BOOST = 10;
class LightRender {
public:
size_t $width;
size_t $height;
Matrix $lightmap;
Matrix $ambient;
Pathing $paths;
matrix::Matrix $fow;
LightRender(Matrix& walls);
void reset_light();
void set_light_target(const Point &at, int value=0);
void clear_light_target(const Point &at);
void path_light(Matrix &walls);
void light_box(LightSource source, Point from, Point &min_out, Point &max_out);
int light_level(int level, float distance, size_t x, size_t y);
void render_light(LightSource source, Point at);
void render_square_light(LightSource source, Point at, PointList &has_light);
void update_fow(Point player_pos, LightSource source);
Matrix &lighting() { return $lightmap; }
Matrix &paths() { return $paths.paths(); }
};
}

72
src/graphics/palette.cpp Normal file
View file

@ -0,0 +1,72 @@
#include <fmt/core.h>
#include "graphics/palette.hpp"
#include "game/config.hpp"
#include "dbc.hpp"
namespace palette {
using std::string;
using nlohmann::json;
struct PaletteMgr {
std::unordered_map<string, sf::Color> palettes;
std::string config;
std::unordered_map<string, string> pending_refs;
bool initialized = false;
};
static PaletteMgr COLOR;
bool initialized() {
return COLOR.initialized;
}
void init(const string &json_file) {
if(!COLOR.initialized) {
COLOR.initialized = true;
COLOR.config = json_file;
auto config = settings::get(json_file);
json& colors = config.json();
for(auto [key, value_specs] : colors.items()) {
const string& base_key = key;
for(auto [value, rgba] : value_specs.items()) {
auto color_path = base_key + ":" + value;
dbc::check(!COLOR.palettes.contains(color_path),
$F("PALLETES config {} already has a color path {}", COLOR.config, color_path));
if(rgba.type() == json::value_t::string) {
COLOR.pending_refs.try_emplace(color_path, rgba);
} else {
uint8_t alpha = rgba.size() == 3 ? 255 : (uint8_t)rgba[3];
sf::Color color{rgba[0], rgba[1], rgba[2], alpha};
COLOR.palettes.try_emplace(color_path, color);
}
}
}
for(auto [color_path, ref] : COLOR.pending_refs) {
dbc::check(COLOR.palettes.contains(ref),
$F("In {} you have {} referring to {} but {} doesn't exist.",
COLOR.config, color_path, ref, ref));
dbc::check(!COLOR.palettes.contains(color_path),
$F("Color {} with ref {} is duplicated.", color_path, ref));
auto color = COLOR.palettes.at(ref);
COLOR.palettes.try_emplace(color_path, color);
}
}
}
sf::Color get(const string& key) {
dbc::check(COLOR.palettes.contains(key),
$F("COLOR {} is missing from {}", key, COLOR.config));
return COLOR.palettes.at(key);
}
sf::Color get(const string& key, const string& value) {
return get(key + ":" + value);
}
}

Some files were not shown because too many files have changed in this diff Show more