First cut of pulling out the relevant parts of my original game to make a little framework.
This commit is contained in:
commit
6a0c9e8d46
177 changed files with 18197 additions and 0 deletions
10
.gdbinit
Normal file
10
.gdbinit
Normal 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
36
.gitignore
vendored
Normal 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
1
.vimrc_proj
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
set makeprg=make\ -f\ ../Makefile\ build
|
||||||
84
Makefile
Normal file
84
Makefile
Normal 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
4
README.md
Normal 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
140
assets/ai.json
Normal 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
132
assets/animation.json
Normal 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
163
assets/cameras.json
Normal 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
117
assets/config.json
Normal 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
46
assets/devices.json
Normal 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
32
assets/enemies.json
Normal 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
12
assets/icons.json
Normal 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
32
assets/items.json
Normal 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
146
assets/map_tiles.json
Normal 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
61
assets/palette.json
Normal 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
7
assets/room_themes.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Plain",
|
||||||
|
"floor": "floor_tile",
|
||||||
|
"walls": "wall_plain"
|
||||||
|
}
|
||||||
|
]
|
||||||
26
assets/shaders.json
Normal file
26
assets/shaders.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
79
assets/shaders/flame_trash.frag
Normal file
79
assets/shaders/flame_trash.frag
Normal 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;
|
||||||
|
}
|
||||||
79
assets/shaders/lightning_attack.frag
Normal file
79
assets/shaders/lightning_attack.frag
Normal 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;
|
||||||
|
}
|
||||||
25
assets/shaders/rayview_sprites.frag
Normal file
25
assets/shaders/rayview_sprites.frag
Normal 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;
|
||||||
|
}
|
||||||
18
assets/shaders/ui_error.frag
Normal file
18
assets/shaders/ui_error.frag
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
assets/shaders/ui_shader.frag
Normal file
29
assets/shaders/ui_shader.frag
Normal 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;
|
||||||
|
}
|
||||||
12
assets/shaders/ui_shape_shader.frag
Normal file
12
assets/shaders/ui_shape_shader.frag
Normal 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
36
assets/tiles.json
Normal 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
142
meson.build
Normal 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)
|
||||||
1
scripts/animation_thing.ps1
Normal file
1
scripts/animation_thing.ps1
Normal 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
41
scripts/build_assets.ps1
Normal 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
|
||||||
13
scripts/coverage_report.ps1
Normal file
13
scripts/coverage_report.ps1
Normal 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
|
||||||
7
scripts/coverage_reset.ps1
Normal file
7
scripts/coverage_reset.ps1
Normal 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
11
scripts/coverage_reset.sh
Normal 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
|
||||||
1020
scripts/gcovr_patched_coverage.py
Normal file
1020
scripts/gcovr_patched_coverage.py
Normal file
File diff suppressed because it is too large
Load diff
188
scripts/magick/pixelize
Normal file
188
scripts/magick/pixelize
Normal 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
372
scripts/magick/position
Normal 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
268
scripts/magick/splitcrop
Normal 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
514
scripts/magick/stainedglass
Normal 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
|
||||||
10
scripts/magick/texture_setup.sh
Normal file
10
scripts/magick/texture_setup.sh
Normal 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
347
scripts/magick/tileimage
Normal 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
302
scripts/magick/tiler
Normal 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
27
scripts/munge_color.ps1
Normal 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
16
scripts/pixelize.ps1
Normal 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
7
scripts/reset_build.ps1
Normal 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
10
scripts/reset_build.sh
Normal 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
BIN
scripts/win_installer.ifp
Normal file
Binary file not shown.
214
src/ai/ai.cpp
Normal file
214
src/ai/ai.cpp
Normal 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
65
src/ai/ai.hpp
Normal 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
74
src/ai/ai_debug.cpp
Normal 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
10
src/ai/ai_debug.hpp
Normal 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
187
src/ai/goap.cpp
Normal 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
84
src/ai/goap.hpp
Normal 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
247
src/algos/dinkyecs.hpp
Normal 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
44
src/algos/matrix.cpp
Normal 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
44
src/algos/matrix.hpp
Normal 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
509
src/algos/maze.cpp
Normal 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
54
src/algos/maze.hpp
Normal 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
146
src/algos/pathing.cpp
Normal 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
41
src/algos/pathing.hpp
Normal 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
20
src/algos/point.hpp
Normal 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
12
src/algos/rand.cpp
Normal 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
41
src/algos/rand.hpp
Normal 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
674
src/algos/shiterator.hpp
Normal 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
32
src/algos/simplefsm.hpp
Normal 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
138
src/algos/spatialmap.cpp
Normal 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
49
src/algos/spatialmap.hpp
Normal 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
11
src/algos/stats.cpp
Normal 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
59
src/algos/stats.hpp
Normal 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
127
src/combat/battle.cpp
Normal 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
50
src/combat/battle.hpp
Normal 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
16
src/combat/combat.cpp
Normal 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
82
src/constants.hpp
Normal 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
47
src/dbc.cpp
Normal 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
53
src/dbc.hpp
Normal 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
49
src/events.hpp
Normal 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
420
src/game/autowalker.cpp
Normal 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
51
src/game/autowalker.hpp
Normal 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
36
src/game/components.cpp
Normal 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
193
src/game/components.hpp
Normal 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
68
src/game/config.cpp
Normal 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
29
src/game/config.hpp
Normal 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
99
src/game/inventory.cpp
Normal 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
25
src/game/inventory.hpp
Normal 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
45
src/game/json_mods.hpp
Normal 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
134
src/game/level.cpp
Normal 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 ¤t_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
34
src/game/level.hpp
Normal 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 ¤t_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
132
src/game/map.cpp
Normal 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
96
src/game/map.hpp
Normal 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
81
src/game/sound.cpp
Normal 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
26
src/game/sound.hpp
Normal 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
656
src/game/systems.cpp
Normal 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
74
src/game/systems.hpp
Normal 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
256
src/game/worldbuilder.cpp
Normal 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
32
src/game/worldbuilder.hpp
Normal 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
312
src/graphics/animation.cpp
Normal 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
150
src/graphics/animation.hpp
Normal 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
132
src/graphics/camera.cpp
Normal 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
37
src/graphics/camera.hpp
Normal 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
141
src/graphics/easing.cpp
Normal 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
32
src/graphics/easing.hpp
Normal 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
87
src/graphics/lights.cpp
Normal 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
40
src/graphics/lights.hpp
Normal 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
72
src/graphics/palette.cpp
Normal 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
Loading…
Add table
Add a link
Reference in a new issue