Add initial source download
4
.itch.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
[[actions]]
|
||||||
|
name = "play"
|
||||||
|
path = "playscii/playscii_linux.sh"
|
||||||
44
README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# PLAYSCII - an ASCII art and game creation tool
|
||||||
|
|
||||||
|
Playscii (pronounced play-skee) is an art, animation, and game creation tool.
|
||||||
|
The latest version will always be available here:
|
||||||
|
|
||||||
|
* [http://jp.itch.io/playscii](http://jp.itch.io/playscii)
|
||||||
|
* [https://heptapod.host/jp-lebreton/playscii](https://heptapod.host/jp-lebreton/playscii)
|
||||||
|
|
||||||
|
Playscii's main website is here:
|
||||||
|
|
||||||
|
* [https://jplebreton.com/playscii/](https://jplebreton.com/playscii/)
|
||||||
|
|
||||||
|
## Offline documentation
|
||||||
|
|
||||||
|
Playscii now includes its own HTML documentation, which you can find in the
|
||||||
|
docs/html/ subfolder of the folder where this README resides.
|
||||||
|
|
||||||
|
## Online documentation
|
||||||
|
|
||||||
|
The latest version of the HTML documentation resides here:
|
||||||
|
|
||||||
|
[https://jplebreton.com/playscii/howto_main.html](https://jplebreton.com/playscii/howto_main.html)
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
If you run into any issues with Playscii, please report a bug here:
|
||||||
|
|
||||||
|
[https://heptapod.host/jp-lebreton/playscii/issues](https://heptapod.host/jp-lebreton/playscii/issues)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
For possible future features see Playscii's Trello:
|
||||||
|
|
||||||
|
[https://trello.com/b/BLQBXn5H/playscii](https://trello.com/b/BLQBXn5H/playscii)
|
||||||
|
|
||||||
|
Please don't take anything there as a promise, though. If you'd find something
|
||||||
|
on there especially valuable, feel free to vote or comment!
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you've made something cool with Playscii and/or have any suggestions on how
|
||||||
|
to improve it, please let JP know!
|
||||||
|
|
||||||
|
[https://jplebreton.com/#contact_email](https://jplebreton.com/#contact_email)
|
||||||
122
art/blob_shadow.psci
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 0,
|
||||||
|
"camera": [
|
||||||
|
2.0,
|
||||||
|
-2.0,
|
||||||
|
3.50242610515142
|
||||||
|
],
|
||||||
|
"charset": "jpetscii",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 102,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 145,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 145,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 104,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 134,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 177,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 177,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 136,
|
||||||
|
"fg": 1,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 4,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 4
|
||||||
|
}
|
||||||
33
art/default_player_stand.psci
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 0,
|
||||||
|
"camera": [
|
||||||
|
1,
|
||||||
|
-1.0,
|
||||||
|
8.989488752238989
|
||||||
|
],
|
||||||
|
"charset": "jpetscii",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 252,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 1,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 1
|
||||||
|
}
|
||||||
694
art/game_object_default.psci
Normal file
|
|
@ -0,0 +1,694 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 0,
|
||||||
|
"camera": [
|
||||||
|
9.78410337431772,
|
||||||
|
-4.180414323343156,
|
||||||
|
11.151385425870988
|
||||||
|
],
|
||||||
|
"charset": "ui",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 10,
|
||||||
|
"xform": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 11,
|
||||||
|
"xform": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 31,
|
||||||
|
"fg": 3,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 32,
|
||||||
|
"fg": 4,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 33,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 28,
|
||||||
|
"fg": 6,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 48,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 39,
|
||||||
|
"fg": 8,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 47,
|
||||||
|
"fg": 9,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 87,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 73,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 100,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 74,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 92,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 14,
|
||||||
|
"xform": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 15,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Layer 2",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 131,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0.1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 5,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 11
|
||||||
|
}
|
||||||
8265
art/hello1.psci
Normal file
81
art/loc_marker.psci
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 0,
|
||||||
|
"camera": [
|
||||||
|
1.5,
|
||||||
|
-1.5,
|
||||||
|
7.369075414190879
|
||||||
|
],
|
||||||
|
"charset": "c64_petscii",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 30,
|
||||||
|
"fg": 16,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 31,
|
||||||
|
"fg": 13,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 75,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 31,
|
||||||
|
"fg": 16,
|
||||||
|
"xform": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 30,
|
||||||
|
"fg": 13,
|
||||||
|
"xform": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 3,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 3
|
||||||
|
}
|
||||||
9631
art/new.psci
Normal file
BIN
art/owell.ed
Normal file
226
art/trigger_default.psci
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 1,
|
||||||
|
"camera": [
|
||||||
|
2.0,
|
||||||
|
-2.0,
|
||||||
|
11.149579543758225
|
||||||
|
],
|
||||||
|
"charset": "c64_petscii",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 20,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 20,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 18,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 9,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 7,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 9,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 7,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 5,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collision",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 0,
|
||||||
|
"char": 94,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0.1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 4,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 4
|
||||||
|
}
|
||||||
117
art/world_properties_object.psci
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
{
|
||||||
|
"active_frame": 0,
|
||||||
|
"active_layer": 0,
|
||||||
|
"camera": [
|
||||||
|
2.5,
|
||||||
|
-1.5,
|
||||||
|
8.762620393409348
|
||||||
|
],
|
||||||
|
"charset": "ui",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"delay": 0.1,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"name": "Layer 1",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 50,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 42,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 45,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 39,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 31,
|
||||||
|
"fg": 2,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 7,
|
||||||
|
"char": 130,
|
||||||
|
"fg": 6,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 7,
|
||||||
|
"char": 129,
|
||||||
|
"fg": 6,
|
||||||
|
"xform": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 128,
|
||||||
|
"fg": 7,
|
||||||
|
"xform": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg": 1,
|
||||||
|
"char": 0,
|
||||||
|
"fg": 0,
|
||||||
|
"xform": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visible": 1,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"height": 3,
|
||||||
|
"palette": "c64_original",
|
||||||
|
"width": 5
|
||||||
|
}
|
||||||
57
art_export.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from art import ART_DIR
|
||||||
|
|
||||||
|
class ArtExporter:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Class for exporting an Art into a non-Playscii format.
|
||||||
|
Export logic happens in run_export; exporter authors simply extend this
|
||||||
|
class, override run_export and the class properties below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
format_name = 'ERROR - ArtExporter.format_name'
|
||||||
|
"User-visible name for this format, shown in export chooser."
|
||||||
|
format_description = "ERROR - ArtExporter.format_description"
|
||||||
|
"String (can be triple-quoted) describing format, shown in export chooser."
|
||||||
|
file_extension = ''
|
||||||
|
"Extension to give the exported file, sans dot."
|
||||||
|
options_dialog_class = None
|
||||||
|
"UIDialog subclass exposing export options to user."
|
||||||
|
|
||||||
|
def __init__(self, app, out_filename, options={}):
|
||||||
|
self.app = app
|
||||||
|
self.art = self.app.ui.active_art
|
||||||
|
# add file extension to output filename if not present
|
||||||
|
if self.file_extension and not out_filename.endswith('.%s' % self.file_extension):
|
||||||
|
out_filename += '.%s' % self.file_extension
|
||||||
|
# output filename in documents/art dir
|
||||||
|
if not out_filename.startswith(self.app.documents_dir + ART_DIR):
|
||||||
|
out_filename = self.app.documents_dir + ART_DIR + out_filename
|
||||||
|
self.success = False
|
||||||
|
"Set True on successful export."
|
||||||
|
# store final filename for log messages
|
||||||
|
self.out_filename = out_filename
|
||||||
|
# remove any cursor-hover changes to art in memory
|
||||||
|
for edit in self.app.cursor.preview_edits:
|
||||||
|
edit.undo()
|
||||||
|
try:
|
||||||
|
if self.run_export(out_filename, options):
|
||||||
|
self.success = True
|
||||||
|
else:
|
||||||
|
line = '%s failed to export %s, see console for errors' % (self.__class__.__name__, out_filename)
|
||||||
|
self.app.log(line)
|
||||||
|
self.app.ui.message_line.post_line(line, hold_time=10, error=True)
|
||||||
|
except:
|
||||||
|
for line in traceback.format_exc().split('\n'):
|
||||||
|
self.app.log(line)
|
||||||
|
# store last used export options for "Export last"
|
||||||
|
self.app.last_export_options = options
|
||||||
|
|
||||||
|
def run_export(self, out_filename, options):
|
||||||
|
"""
|
||||||
|
Contains the actual export logic. Write data based on current art,
|
||||||
|
return success.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
88
art_import.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
import os, traceback
|
||||||
|
|
||||||
|
from art import Art, ART_FILE_EXTENSION, DEFAULT_CHARSET, DEFAULT_PALETTE
|
||||||
|
from ui_file_chooser_dialog import GenericImportChooserDialog
|
||||||
|
|
||||||
|
class ArtImporter:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Class for creating a new Art from data in non-Playscii format.
|
||||||
|
Import logic happens in run_import; importer authors simply extend this
|
||||||
|
class, override run_import and the class properties below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
format_name = 'ERROR - ArtImporter.format_name'
|
||||||
|
"User-visible name for this format, shown in import chooser."
|
||||||
|
format_description = "ERROR - ArtImporter.format_description"
|
||||||
|
"String (can be triple-quoted) describing format, shown in import chooser."
|
||||||
|
allowed_file_extensions = []
|
||||||
|
"List of file extensions for this format - if empty, any file is accepted."
|
||||||
|
file_chooser_dialog_class = GenericImportChooserDialog
|
||||||
|
"""
|
||||||
|
BaseFileChooserDialog subclass for picking files. Only needed for things
|
||||||
|
like custom preview images.
|
||||||
|
"""
|
||||||
|
options_dialog_class = None
|
||||||
|
"UIDialog subclass exposing import options to user."
|
||||||
|
generic_error = '%s failed to import %s'
|
||||||
|
# if False (eg bitmap conversion), "Imported successfully" message
|
||||||
|
# won't show on successful creation
|
||||||
|
completes_instantly = True
|
||||||
|
|
||||||
|
def __init__(self, app, in_filename, options={}):
|
||||||
|
self.app = app
|
||||||
|
new_filename = '%s.%s' % (os.path.splitext(in_filename)[0],
|
||||||
|
ART_FILE_EXTENSION)
|
||||||
|
self.art = self.app.new_art(new_filename)
|
||||||
|
# use charset and palette of existing art
|
||||||
|
charset = self.app.ui.active_art.charset if self.app.ui.active_art else self.app.load_charset(DEFAULT_CHARSET)
|
||||||
|
self.art.set_charset(charset)
|
||||||
|
palette = self.app.ui.active_art.palette if self.app.ui.active_art else self.app.load_palette(DEFAULT_PALETTE)
|
||||||
|
self.art.set_palette(palette)
|
||||||
|
self.app.set_new_art_for_edit(self.art)
|
||||||
|
self.art.clear_frame_layer(0, 0, 1)
|
||||||
|
self.success = False
|
||||||
|
"Set True on successful import."
|
||||||
|
# run_import returns success, log it separately from exceptions
|
||||||
|
try:
|
||||||
|
if self.run_import(in_filename, options):
|
||||||
|
self.success = True
|
||||||
|
except:
|
||||||
|
for line in traceback.format_exc().split('\n'):
|
||||||
|
self.app.log(line)
|
||||||
|
if not self.success:
|
||||||
|
line = self.generic_error % (self.__class__.__name__, in_filename)
|
||||||
|
self.app.log(line)
|
||||||
|
self.app.close_art(self.art)
|
||||||
|
# post message now after close_art sets active art back
|
||||||
|
self.app.ui.message_line.post_line(line, error=True)
|
||||||
|
return
|
||||||
|
# tidy final result, whether or not it was successful
|
||||||
|
# TODO: GROSS! figure out why this works but
|
||||||
|
# art.geo_changed=True and art.mark_all_frames_changed() don't!
|
||||||
|
self.app.ui.erase_selection_or_art()
|
||||||
|
self.app.ui.undo()
|
||||||
|
# adjust for new art size and set it active
|
||||||
|
self.app.ui.adjust_for_art_resize(self.art)
|
||||||
|
self.app.ui.set_active_art(self.art)
|
||||||
|
|
||||||
|
def set_art_charset(self, charset_name):
|
||||||
|
"Convenience function for setting charset by name from run_import."
|
||||||
|
self.art.set_charset_by_name(charset_name)
|
||||||
|
|
||||||
|
def set_art_palette(self, palette_name):
|
||||||
|
"Convenience function for setting palette by name from run_import."
|
||||||
|
self.art.set_palette_by_name(palette_name)
|
||||||
|
|
||||||
|
def resize(self, new_width, new_height):
|
||||||
|
"Convenience function for resizing art from run_import"
|
||||||
|
self.art.resize(new_width, new_height)
|
||||||
|
self.app.ui.adjust_for_art_resize(self.art)
|
||||||
|
|
||||||
|
def run_import(self, in_filename, options):
|
||||||
|
"""
|
||||||
|
Contains the actual import logic. Read input file, set Art
|
||||||
|
size/charset/palette, set tiles from data, return success.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
28
artscripts/c64_ed_to_orig.arsc
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
# convert from C64 EDSCII to C64 original color palettes
|
||||||
|
|
||||||
|
color_map = {
|
||||||
|
1: 1,
|
||||||
|
2: 3,
|
||||||
|
3: 5,
|
||||||
|
4: 7,
|
||||||
|
5: 6,
|
||||||
|
6: 8,
|
||||||
|
7: 12,
|
||||||
|
8: 16,
|
||||||
|
9: 10,
|
||||||
|
10: 9,
|
||||||
|
11: 11,
|
||||||
|
12: 15,
|
||||||
|
13: 14,
|
||||||
|
14: 4,
|
||||||
|
15: 13,
|
||||||
|
16: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
for frame, layer, x, y in TileIter(self):
|
||||||
|
ch, fg, bg, xf = self.get_tile_at(frame, layer, x, y)
|
||||||
|
fg = color_map.get(fg, 0)
|
||||||
|
bg = color_map.get(bg, 0)
|
||||||
|
self.set_color_at(frame, layer, x, y, fg, fg=True)
|
||||||
|
self.set_color_at(frame, layer, x, y, bg, fg=False)
|
||||||
28
artscripts/c64_fade.arsc
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
# palette-specific (c64 original) fade to black
|
||||||
|
|
||||||
|
color_ramps = {
|
||||||
|
2: 16,
|
||||||
|
3: 10,
|
||||||
|
4: 15,
|
||||||
|
5: 12,
|
||||||
|
6: 12,
|
||||||
|
7: 12,
|
||||||
|
8: 9,
|
||||||
|
9: 10,
|
||||||
|
10: 12,
|
||||||
|
11: 3,
|
||||||
|
12: 1,
|
||||||
|
13: 12,
|
||||||
|
14: 6,
|
||||||
|
15: 7,
|
||||||
|
16: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
for frame, layer, x, y in TileIter(self):
|
||||||
|
fg = self.get_fg_color_index_at(frame, layer, x, y)
|
||||||
|
fg = color_ramps.get(fg, 1)
|
||||||
|
self.set_color_at(frame, layer, x, y, fg, fg=True)
|
||||||
|
bg = self.get_bg_color_index_at(frame, layer, x, y)
|
||||||
|
bg = color_ramps.get(bg, 1)
|
||||||
|
self.set_color_at(frame, layer, x, y, bg, fg=False)
|
||||||
39
artscripts/conway.arsc
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# conway's game of life
|
||||||
|
|
||||||
|
# naive, super slow implementation for proof-of-concept
|
||||||
|
# (accessing char array would probably be way faster!)
|
||||||
|
for frame, layer, x, y in TileIter(self):
|
||||||
|
dead = self.get_char_index_at(frame, layer, x, y) == 0
|
||||||
|
# N, NE, E, SE, S, SW, W, NW
|
||||||
|
neighbor_offsets = [(0, -1), (1, -1), (1, 0), (1, 1),
|
||||||
|
(0, 1), (-1, 1), (-1, 0), (-1, -1)]
|
||||||
|
neighbors = 0
|
||||||
|
neighbor_chars = []
|
||||||
|
neighbor_colors = []
|
||||||
|
for offset in neighbor_offsets:
|
||||||
|
check_x, check_y = x + offset[0], y + offset[1]
|
||||||
|
# don't check at edges
|
||||||
|
if not (0 < check_x < self.width and 0 < check_y < self.height):
|
||||||
|
continue
|
||||||
|
neighbor_char = self.get_char_index_at(frame, layer, check_x, check_y)
|
||||||
|
if neighbor_char != 0:
|
||||||
|
neighbors += 1
|
||||||
|
# remember neighbor char in case we come alive
|
||||||
|
neighbor_chars.append(neighbor_char)
|
||||||
|
fg = self.get_fg_color_index_at(frame, layer, check_x, check_y)
|
||||||
|
neighbor_colors.append(fg)
|
||||||
|
bg = self.get_bg_color_index_at(frame, layer, check_x, check_y)
|
||||||
|
neighbor_colors.append(bg)
|
||||||
|
# rule #4: any dead cell with exactly 3 neighbors becomes alive
|
||||||
|
if dead and neighbors == 3:
|
||||||
|
# pick a random neighbord character to be
|
||||||
|
self.set_char_index_at(frame, layer, x, y, random.choice(neighbor_chars))
|
||||||
|
change_fg = random.choice([False, True])
|
||||||
|
self.set_color_at(frame, layer, x, y, random.choice(neighbor_colors), change_fg)
|
||||||
|
# rule #3: any living cell with >3 neighbors dies from overcrowding
|
||||||
|
elif neighbors > 3:
|
||||||
|
self.set_char_index_at(frame, layer, x, y, 0)
|
||||||
|
# rule #1: any living cell with <2 neighbors dies from underpopulation
|
||||||
|
elif neighbors < 2:
|
||||||
|
self.set_char_index_at(frame, layer, x, y, 0)
|
||||||
|
# rule #2: any living cell with 2 or 3 neighbors survives
|
||||||
23
artscripts/dissolv.arsc
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
# quickie dissolve effect
|
||||||
|
|
||||||
|
frame, layer = 0, 0
|
||||||
|
|
||||||
|
left_x = int(self.width / 2)
|
||||||
|
top_y = int(self.height / 2)
|
||||||
|
|
||||||
|
for x in range(int(self.width / 2)):
|
||||||
|
for y in range(self.height):
|
||||||
|
char = self.get_char_index_at(frame, layer, x, y)
|
||||||
|
char = max(0, char - 1)
|
||||||
|
if x > 0:
|
||||||
|
self.set_char_index_at(frame, layer, x - 1, y, char)
|
||||||
|
self.set_char_index_at(frame, layer, x, y, 0)
|
||||||
|
|
||||||
|
for x in range(self.width - 1, int(self.width / 2), -1):
|
||||||
|
for y in range(self.height):
|
||||||
|
char = self.get_char_index_at(frame, layer, x, y)
|
||||||
|
char = max(0, char - 1)
|
||||||
|
if x < self.width - 1:
|
||||||
|
self.set_char_index_at(frame, layer, x + 1, y, char)
|
||||||
|
self.set_char_index_at(frame, layer, x, y, 0)
|
||||||
14
artscripts/evap.arsc
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
# quickie "evaporate" effect
|
||||||
|
|
||||||
|
spesh_idx = 127
|
||||||
|
|
||||||
|
for frame, layer, x, y in TileIter(self):
|
||||||
|
char = self.get_char_index_at(frame, layer, x, y)
|
||||||
|
if char != spesh_idx and char != 0:
|
||||||
|
self.set_char_index_at(frame, layer, x, y, spesh_idx)
|
||||||
|
elif y < self.height - 1:
|
||||||
|
c,f,b,xf = self.get_tile_at(frame, layer, x, y+1)
|
||||||
|
self.set_tile_at(frame, layer, x, y, c, f, b)
|
||||||
|
elif y == self.height - 1:
|
||||||
|
self.set_char_index_at(frame, layer, x, y, 0)
|
||||||
202
artscripts/hello1.arsc
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# hello1 test art generator script
|
||||||
|
# every line in this file must be a valid python expression.
|
||||||
|
# "self" is the Art that's running us,
|
||||||
|
# so we have full access to its namespace!
|
||||||
|
|
||||||
|
# sets some test data:
|
||||||
|
# c64_edscii charset & palette
|
||||||
|
# add tiles/layers if not 8x8, 3 layers
|
||||||
|
|
||||||
|
self.set_charset_by_name('c64_edscii')
|
||||||
|
self.set_palette_by_name('c64_edscii')
|
||||||
|
|
||||||
|
if self.layers < 3:
|
||||||
|
self.add_layer(0.25)
|
||||||
|
self.add_layer(0.5)
|
||||||
|
|
||||||
|
if self.width < 8 or self.height < 8:
|
||||||
|
self.resize(8, 8)
|
||||||
|
|
||||||
|
# clear 1st layer to black, 2nd and 3rd to transparent
|
||||||
|
self.clear_frame_layer(0, 0, self.palette.darkest_index)
|
||||||
|
self.clear_frame_layer(0, 1)
|
||||||
|
self.clear_frame_layer(0, 2)
|
||||||
|
# write white text onto 3 layers
|
||||||
|
color = self.palette.lightest_index
|
||||||
|
self.write_string(0, 0, 1, 1, 'Hello.', color)
|
||||||
|
self.set_char_transform_at(0, 0, 2, 1, UV_ROTATE90)
|
||||||
|
# draw snaky ring thingy
|
||||||
|
# color ramp: 2, 10, 6, 13, 14, 12, 3, back to 2
|
||||||
|
# top
|
||||||
|
self.set_tile_at(0, 1, 1, 3, 119, 2)
|
||||||
|
self.set_tile_at(0, 1, 2, 3, 102, 10)
|
||||||
|
self.set_tile_at(0, 1, 3, 3, 102, 6)
|
||||||
|
self.set_tile_at(0, 1, 4, 3, 102, 13)
|
||||||
|
self.set_tile_at(0, 1, 5, 3, 120, 14)
|
||||||
|
# sides
|
||||||
|
self.set_tile_at(0, 1, 1, 4, 145, 3)
|
||||||
|
self.set_tile_at(0, 1, 5, 4, 145, 12)
|
||||||
|
self.set_tile_at(0, 1, 1, 5, 145, 12)
|
||||||
|
self.set_tile_at(0, 1, 5, 5, 145, 3)
|
||||||
|
# bottom
|
||||||
|
self.set_tile_at(0, 1, 1, 6, 121, 14)
|
||||||
|
self.set_tile_at(0, 1, 2, 6, 102, 13)
|
||||||
|
self.set_tile_at(0, 1, 3, 6, 102, 6)
|
||||||
|
self.set_tile_at(0, 1, 4, 6, 102, 10)
|
||||||
|
self.set_tile_at(0, 1, 5, 6, 122, 2)
|
||||||
|
# :]
|
||||||
|
char = self.charset.get_char_index(':')
|
||||||
|
self.set_tile_at(0, 2, 3, 4, char, color)
|
||||||
|
char = self.charset.get_char_index(']')
|
||||||
|
self.set_tile_at(0, 2, 4, 4, char, color)
|
||||||
|
|
||||||
|
# add frames and animate 'em
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
self.duplicate_frame(0)
|
||||||
|
|
||||||
|
# cycle capitals through "hello" text
|
||||||
|
h = self.charset.get_char_index('h')
|
||||||
|
char = self.charset.get_char_index('E')
|
||||||
|
self.set_char_index_at(1, 0, 2, 1, char)
|
||||||
|
self.set_char_index_at(1, 0, 1, 1, h)
|
||||||
|
char = self.charset.get_char_index('L')
|
||||||
|
self.set_char_index_at(2, 0, 3, 1, char)
|
||||||
|
self.set_char_index_at(2, 0, 1, 1, h)
|
||||||
|
self.set_char_index_at(3, 0, 4, 1, char)
|
||||||
|
self.set_char_index_at(3, 0, 1, 1, h)
|
||||||
|
char = self.charset.get_char_index('O')
|
||||||
|
self.set_char_index_at(4, 0, 5, 1, char)
|
||||||
|
self.set_char_index_at(4, 0, 1, 1, h)
|
||||||
|
char = self.charset.get_char_index('!')
|
||||||
|
self.set_char_index_at(5, 0, 6, 1, char)
|
||||||
|
self.set_char_index_at(5, 0, 1, 1, h)
|
||||||
|
self.set_char_index_at(6, 0, 1, 1, h)
|
||||||
|
# make smiley go from ;] to :D
|
||||||
|
char = self.charset.get_char_index(';')
|
||||||
|
self.set_char_index_at(3, 2, 3, 4, char)
|
||||||
|
self.set_char_index_at(4, 2, 3, 4, char)
|
||||||
|
self.set_char_index_at(5, 2, 3, 4, char)
|
||||||
|
char = self.charset.get_char_index('D')
|
||||||
|
self.set_char_index_at(3, 2, 4, 4, char)
|
||||||
|
self.set_char_index_at(4, 2, 4, 4, char)
|
||||||
|
self.set_char_index_at(5, 2, 4, 4, char)
|
||||||
|
self.set_char_transform_at(4, 2, 4, 4, UV_FLIPX)
|
||||||
|
# cycle colors for snaky thing
|
||||||
|
#
|
||||||
|
# frame 1 top
|
||||||
|
#
|
||||||
|
self.set_color_at(1, 1, 1, 3, 10)
|
||||||
|
self.set_color_at(1, 1, 2, 3, 6)
|
||||||
|
self.set_color_at(1, 1, 3, 3, 13)
|
||||||
|
self.set_color_at(1, 1, 4, 3, 14)
|
||||||
|
self.set_color_at(1, 1, 5, 3, 12)
|
||||||
|
# frame 1 sides
|
||||||
|
self.set_color_at(1, 1, 1, 4, 2)
|
||||||
|
self.set_color_at(1, 1, 5, 4, 3)
|
||||||
|
self.set_color_at(1, 1, 1, 5, 3)
|
||||||
|
self.set_color_at(1, 1, 5, 5, 2)
|
||||||
|
# frame 1 bottom
|
||||||
|
self.set_color_at(1, 1, 1, 6, 12)
|
||||||
|
self.set_color_at(1, 1, 2, 6, 14)
|
||||||
|
self.set_color_at(1, 1, 3, 6, 13)
|
||||||
|
self.set_color_at(1, 1, 4, 6, 6)
|
||||||
|
self.set_color_at(1, 1, 5, 6, 10)
|
||||||
|
#
|
||||||
|
# frame 2 top
|
||||||
|
#
|
||||||
|
self.set_color_at(2, 1, 1, 3, 6)
|
||||||
|
self.set_color_at(2, 1, 2, 3, 13)
|
||||||
|
self.set_color_at(2, 1, 3, 3, 14)
|
||||||
|
self.set_color_at(2, 1, 4, 3, 12)
|
||||||
|
self.set_color_at(2, 1, 5, 3, 3)
|
||||||
|
# frame 2 sides
|
||||||
|
self.set_color_at(2, 1, 1, 4, 10)
|
||||||
|
self.set_color_at(2, 1, 5, 4, 2)
|
||||||
|
self.set_color_at(2, 1, 1, 5, 2)
|
||||||
|
self.set_color_at(2, 1, 5, 5, 10)
|
||||||
|
# frame 2 bottom
|
||||||
|
self.set_color_at(2, 1, 1, 6, 3)
|
||||||
|
self.set_color_at(2, 1, 2, 6, 12)
|
||||||
|
self.set_color_at(2, 1, 3, 6, 14)
|
||||||
|
self.set_color_at(2, 1, 4, 6, 13)
|
||||||
|
self.set_color_at(2, 1, 5, 6, 6)
|
||||||
|
#
|
||||||
|
# frame 3 top
|
||||||
|
#
|
||||||
|
self.set_color_at(3, 1, 1, 3, 13)
|
||||||
|
self.set_color_at(3, 1, 2, 3, 14)
|
||||||
|
self.set_color_at(3, 1, 3, 3, 12)
|
||||||
|
self.set_color_at(3, 1, 4, 3, 3)
|
||||||
|
self.set_color_at(3, 1, 5, 3, 2)
|
||||||
|
# frame 3 sides
|
||||||
|
self.set_color_at(3, 1, 1, 4, 6)
|
||||||
|
self.set_color_at(3, 1, 5, 4, 10)
|
||||||
|
self.set_color_at(3, 1, 1, 5, 10)
|
||||||
|
self.set_color_at(3, 1, 5, 5, 6)
|
||||||
|
# frame 3 bottom
|
||||||
|
self.set_color_at(3, 1, 1, 6, 2)
|
||||||
|
self.set_color_at(3, 1, 2, 6, 3)
|
||||||
|
self.set_color_at(3, 1, 3, 6, 12)
|
||||||
|
self.set_color_at(3, 1, 4, 6, 14)
|
||||||
|
self.set_color_at(3, 1, 5, 6, 13)
|
||||||
|
#
|
||||||
|
# frame 4 top
|
||||||
|
#
|
||||||
|
self.set_color_at(4, 1, 1, 3, 14)
|
||||||
|
self.set_color_at(4, 1, 2, 3, 12)
|
||||||
|
self.set_color_at(4, 1, 3, 3, 3)
|
||||||
|
self.set_color_at(4, 1, 4, 3, 2)
|
||||||
|
self.set_color_at(4, 1, 5, 3, 10)
|
||||||
|
# frame 4 sides
|
||||||
|
self.set_color_at(4, 1, 1, 4, 13)
|
||||||
|
self.set_color_at(4, 1, 5, 4, 6)
|
||||||
|
self.set_color_at(4, 1, 1, 5, 6)
|
||||||
|
self.set_color_at(4, 1, 5, 5, 13)
|
||||||
|
# frame 4 bottom
|
||||||
|
self.set_color_at(4, 1, 1, 6, 10)
|
||||||
|
self.set_color_at(4, 1, 2, 6, 2)
|
||||||
|
self.set_color_at(4, 1, 3, 6, 3)
|
||||||
|
self.set_color_at(4, 1, 4, 6, 12)
|
||||||
|
self.set_color_at(4, 1, 5, 6, 14)
|
||||||
|
#
|
||||||
|
# frame 5 top
|
||||||
|
#
|
||||||
|
self.set_color_at(5, 1, 1, 3, 12)
|
||||||
|
self.set_color_at(5, 1, 2, 3, 3)
|
||||||
|
self.set_color_at(5, 1, 3, 3, 2)
|
||||||
|
self.set_color_at(5, 1, 4, 3, 10)
|
||||||
|
self.set_color_at(5, 1, 5, 3, 6)
|
||||||
|
# frame 5 sides
|
||||||
|
self.set_color_at(5, 1, 1, 4, 14)
|
||||||
|
self.set_color_at(5, 1, 5, 4, 13)
|
||||||
|
self.set_color_at(5, 1, 1, 5, 13)
|
||||||
|
self.set_color_at(5, 1, 5, 5, 14)
|
||||||
|
# frame 5 bottom
|
||||||
|
self.set_color_at(5, 1, 1, 6, 6)
|
||||||
|
self.set_color_at(5, 1, 2, 6, 10)
|
||||||
|
self.set_color_at(5, 1, 3, 6, 2)
|
||||||
|
self.set_color_at(5, 1, 4, 6, 3)
|
||||||
|
self.set_color_at(5, 1, 5, 6, 12)
|
||||||
|
#
|
||||||
|
# frame 6 top
|
||||||
|
#
|
||||||
|
self.set_color_at(6, 1, 1, 3, 3)
|
||||||
|
self.set_color_at(6, 1, 2, 3, 2)
|
||||||
|
self.set_color_at(6, 1, 3, 3, 10)
|
||||||
|
self.set_color_at(6, 1, 4, 3, 6)
|
||||||
|
self.set_color_at(6, 1, 5, 3, 13)
|
||||||
|
# frame 6 sides
|
||||||
|
self.set_color_at(6, 1, 1, 4, 12)
|
||||||
|
self.set_color_at(6, 1, 5, 4, 14)
|
||||||
|
self.set_color_at(6, 1, 1, 5, 14)
|
||||||
|
self.set_color_at(6, 1, 5, 5, 12)
|
||||||
|
# frame 6 bottom
|
||||||
|
self.set_color_at(6, 1, 1, 6, 13)
|
||||||
|
self.set_color_at(6, 1, 2, 6, 6)
|
||||||
|
self.set_color_at(6, 1, 3, 6, 10)
|
||||||
|
self.set_color_at(6, 1, 4, 6, 2)
|
||||||
|
self.set_color_at(6, 1, 5, 6, 3)
|
||||||
13
artscripts/mutate.arsc
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
# mutate!
|
||||||
|
# change a random tile on a random layer
|
||||||
|
|
||||||
|
x = random.randint(0, self.width-1)
|
||||||
|
y = random.randint(0, self.height-1)
|
||||||
|
layer = random.randint(0, self.layers-1)
|
||||||
|
char = random.randint(0, 128)
|
||||||
|
color_index = self.palette.get_random_color_index()
|
||||||
|
self.set_char_index_at(0, layer, x, y, char)
|
||||||
|
self.set_color_at(0, layer, x, y, color_index)
|
||||||
|
color_index = self.palette.get_random_color_index()
|
||||||
|
self.set_color_at(0, layer, x, y, color_index, False)
|
||||||
145
audio.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from sdl2 import sdlmixer
|
||||||
|
|
||||||
|
class PlayingSound:
|
||||||
|
"represents a currently playing sound"
|
||||||
|
def __init__(self, filename, channel, game_object, looping=False):
|
||||||
|
self.filename = filename
|
||||||
|
self.channel = channel
|
||||||
|
self.go = game_object
|
||||||
|
self.looping = looping
|
||||||
|
|
||||||
|
class AudioLord:
|
||||||
|
|
||||||
|
sample_rate = 44100
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
# initialize audio
|
||||||
|
sdlmixer.Mix_Init(sdlmixer.MIX_INIT_OGG|sdlmixer.MIX_INIT_MOD)
|
||||||
|
sdlmixer.Mix_OpenAudio(self.sample_rate, sdlmixer.MIX_DEFAULT_FORMAT,
|
||||||
|
2, 1024)
|
||||||
|
self.reset()
|
||||||
|
# sound callback
|
||||||
|
# retain handle to C callable even though we don't use it directly
|
||||||
|
self.sound_cb = ctypes.CFUNCTYPE(None, ctypes.c_int)(self.channel_finished)
|
||||||
|
sdlmixer.Mix_ChannelFinished(self.sound_cb)
|
||||||
|
|
||||||
|
def channel_finished(self, channel):
|
||||||
|
# remove sound from dicts of playing channels and sounds
|
||||||
|
old_sound = self.playing_channels.pop(channel)
|
||||||
|
self.playing_sounds[old_sound.filename].remove(old_sound)
|
||||||
|
# remove empty list
|
||||||
|
if self.playing_sounds[old_sound.filename] == []:
|
||||||
|
self.playing_sounds.pop(old_sound.filename)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.stop_all_music()
|
||||||
|
self.stop_all_sounds()
|
||||||
|
# current playing sounds, of form:
|
||||||
|
# {'filename': [list of PlayingSound objects]}
|
||||||
|
self.playing_sounds = {}
|
||||||
|
# current playing channels, of form:
|
||||||
|
# {channel_number: PlayingSound object}
|
||||||
|
self.playing_channels = {}
|
||||||
|
# handle init case where self.musics doesn't exist yet
|
||||||
|
if hasattr(self, 'musics'):
|
||||||
|
for music in self.musics.values():
|
||||||
|
sdlmixer.Mix_FreeMusic(music)
|
||||||
|
self.musics = {}
|
||||||
|
if hasattr(self, 'sounds'):
|
||||||
|
for sound in self.sounds.values():
|
||||||
|
sdlmixer.Mix_FreeChunk(sound)
|
||||||
|
self.sounds = {}
|
||||||
|
|
||||||
|
def register_sound(self, sound_filename):
|
||||||
|
if sound_filename in self.sounds:
|
||||||
|
return self.sounds[sound_filename]
|
||||||
|
new_sound = sdlmixer.Mix_LoadWAV(bytes(sound_filename, 'utf-8'))
|
||||||
|
self.sounds[sound_filename] = new_sound
|
||||||
|
return new_sound
|
||||||
|
|
||||||
|
def object_play_sound(self, game_object, sound_filename,
|
||||||
|
loops=0, allow_multiple=False):
|
||||||
|
# TODO: volume param? sdlmixer.MIX_MAX_VOLUME if not specified
|
||||||
|
# bail if same object isn't allowed to play same sound multiple times
|
||||||
|
if not allow_multiple and sound_filename in self.playing_sounds:
|
||||||
|
for playing_sound in self.playing_sounds[sound_filename]:
|
||||||
|
if playing_sound.go is game_object:
|
||||||
|
return
|
||||||
|
sound = self.register_sound(sound_filename)
|
||||||
|
channel = sdlmixer.Mix_PlayChannel(-1, sound, loops)
|
||||||
|
# add sound to dicts of playing sounds and channels
|
||||||
|
new_playing_sound = PlayingSound(sound_filename, channel, game_object,
|
||||||
|
loops == -1)
|
||||||
|
if sound_filename in self.playing_sounds:
|
||||||
|
self.playing_sounds[sound_filename].append(new_playing_sound)
|
||||||
|
else:
|
||||||
|
self.playing_sounds[sound_filename] = [new_playing_sound]
|
||||||
|
self.playing_channels[channel] = new_playing_sound
|
||||||
|
|
||||||
|
def object_stop_sound(self, game_object, sound_filename):
|
||||||
|
if not sound_filename in self.playing_sounds:
|
||||||
|
return
|
||||||
|
# stop all instances of this sound object might be playing
|
||||||
|
for sound in self.playing_sounds[sound_filename]:
|
||||||
|
if game_object is sound.go:
|
||||||
|
sdlmixer.Mix_HaltChannel(sound.channel)
|
||||||
|
|
||||||
|
def object_stop_all_sounds(self, game_object):
|
||||||
|
sounds_to_stop = []
|
||||||
|
for sound_filename,sounds in self.playing_sounds.items():
|
||||||
|
for sound in sounds:
|
||||||
|
if sound.go is game_object:
|
||||||
|
sounds_to_stop.append(sound_filename)
|
||||||
|
for sound_filename in sounds_to_stop:
|
||||||
|
self.object_stop_sound(game_object, sound_filename)
|
||||||
|
|
||||||
|
def stop_all_sounds(self):
|
||||||
|
sdlmixer.Mix_HaltChannel(-1)
|
||||||
|
|
||||||
|
def set_music(self, music_filename):
|
||||||
|
if music_filename in self.musics:
|
||||||
|
return
|
||||||
|
new_music = sdlmixer.Mix_LoadMUS(bytes(music_filename, 'utf-8'))
|
||||||
|
self.musics[music_filename] = new_music
|
||||||
|
|
||||||
|
def start_music(self, music_filename, loops=-1):
|
||||||
|
# TODO: fade in support etc
|
||||||
|
music = self.musics[music_filename]
|
||||||
|
sdlmixer.Mix_PlayMusic(music, loops)
|
||||||
|
self.current_music = music_filename
|
||||||
|
|
||||||
|
def pause_music(self):
|
||||||
|
if self.current_music:
|
||||||
|
sdlmixer.Mix_PauseMusic()
|
||||||
|
|
||||||
|
def resume_music(self):
|
||||||
|
if self.current_music:
|
||||||
|
sdlmixer.Mix_ResumeMusic()
|
||||||
|
|
||||||
|
def stop_music(self, music_filename):
|
||||||
|
# TODO: fade out support
|
||||||
|
sdlmixer.Mix_HaltMusic()
|
||||||
|
self.current_music = None
|
||||||
|
|
||||||
|
def is_music_playing(self):
|
||||||
|
return bool(sdlmixer.Mix_PlayingMusic())
|
||||||
|
|
||||||
|
def resume_music(self):
|
||||||
|
if self.current_music:
|
||||||
|
sdlmixer.Mix_ResumeMusic()
|
||||||
|
|
||||||
|
def stop_all_music(self):
|
||||||
|
sdlmixer.Mix_HaltMusic()
|
||||||
|
self.current_music = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.current_music and not self.is_music_playing():
|
||||||
|
self.current_music = None
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
self.reset()
|
||||||
|
sdlmixer.Mix_CloseAudio()
|
||||||
|
sdlmixer.Mix_Quit()
|
||||||
177
binds.cfg.default
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
# DEFAULT BIND FILE TEMPLATE, DO NOT MODIFY
|
||||||
|
# user keybinds file
|
||||||
|
# accepted modifiers: ctrl, alt, shift
|
||||||
|
# keys must be equivalent to output of sdl2.SDL_GetKeyName(),
|
||||||
|
# eg return, tab, backspace
|
||||||
|
|
||||||
|
self.edit_bind_src = {
|
||||||
|
'ctrl q' : 'quit',
|
||||||
|
'`' : 'toggle_console',
|
||||||
|
'ctrl m' : 'import_file',
|
||||||
|
'ctrl e' : ('export_file_last', 'edit_art_for_selected_objects'),
|
||||||
|
'ctrl -' : 'decrease_ui_scale',
|
||||||
|
'ctrl =' : 'increase_ui_scale',
|
||||||
|
'alt return': 'toggle_fullscreen',
|
||||||
|
'1' : 'decrease_brush_size',
|
||||||
|
'2' : 'increase_brush_size',
|
||||||
|
'3' : 'cycle_char_forward',
|
||||||
|
'shift 3' : 'cycle_char_backward',
|
||||||
|
'4' : 'cycle_fg_forward',
|
||||||
|
'shift 4' : 'cycle_fg_backward',
|
||||||
|
'5' : 'cycle_bg_forward',
|
||||||
|
'shift 5' : 'cycle_bg_backward',
|
||||||
|
'6' : 'cycle_xform_forward',
|
||||||
|
'shift 6' : 'cycle_xform_backward',
|
||||||
|
'c' : 'toggle_affects_char',
|
||||||
|
'f' : 'toggle_affects_fg',
|
||||||
|
'b' : 'toggle_affects_bg',
|
||||||
|
# bind can also be a tuple of function names
|
||||||
|
'x' : ('toggle_affects_xform', 'game_frob'),
|
||||||
|
'z' : ('toggle_zoom_extents', 'game_grab'),
|
||||||
|
'shift r' : 'toggle_crt',
|
||||||
|
'a' : 'select_pencil_tool',
|
||||||
|
'e' : 'select_erase_tool',
|
||||||
|
'r' : 'select_rotate_tool',
|
||||||
|
't' : 'select_text_tool',
|
||||||
|
's' : 'select_select_tool',
|
||||||
|
'ctrl x' : 'cut_selection',
|
||||||
|
'ctrl c' : 'copy_selection',
|
||||||
|
'v' : 'select_paste_tool',
|
||||||
|
'ctrl v' : 'select_paste_tool',
|
||||||
|
'i' : 'select_fill_tool',
|
||||||
|
'escape' : 'cancel',
|
||||||
|
'ctrl d' : 'select_none',
|
||||||
|
'ctrl a' : 'select_all',
|
||||||
|
'ctrl i' : 'select_invert',
|
||||||
|
'delete' : 'erase_selection_or_art',
|
||||||
|
'backspace': 'erase_selection_or_art',
|
||||||
|
'g' : 'toggle_game_mode',
|
||||||
|
'shift e' : 'toggle_game_edit_ui',
|
||||||
|
'ctrl shift g': 'set_game_dir',
|
||||||
|
'ctrl g' : 'load_game_state',
|
||||||
|
'f2' : 'reset_game',
|
||||||
|
'space' : 'toggle_picker',
|
||||||
|
'w' : 'swap_fg_bg_colors',
|
||||||
|
'ctrl s' : 'save_current',
|
||||||
|
'shift u' : 'toggle_ui_visibility',
|
||||||
|
'shift g' : 'toggle_grid_visibility',
|
||||||
|
',' : 'previous_frame',
|
||||||
|
'.' : 'next_frame',
|
||||||
|
'p' : 'toggle_anim_playback',
|
||||||
|
'[' : 'previous_layer',
|
||||||
|
']' : 'next_layer',
|
||||||
|
'shift ctrl tab': 'previous_art',
|
||||||
|
'ctrl tab' : 'next_art',
|
||||||
|
'ctrl z' : 'undo',
|
||||||
|
'shift ctrl z': 'redo',
|
||||||
|
'q' : 'quick_grab',
|
||||||
|
'shift t' : 'toggle_camera_tilt',
|
||||||
|
'shift i' : 'toggle_overlay_image',
|
||||||
|
'=' : 'camera_zoom_in_proportional',
|
||||||
|
'-' : 'camera_zoom_out_proportional',
|
||||||
|
'return' : 'select_or_paint',
|
||||||
|
'shift return': 'add_to_list_selection',
|
||||||
|
'ctrl return': 'remove_from_list_selection',
|
||||||
|
'f12' : 'screenshot',
|
||||||
|
'ctrl shift m' : 'run_test_mutate',
|
||||||
|
'up' : 'arrow_up',
|
||||||
|
'down' : 'arrow_down',
|
||||||
|
'left' : 'arrow_left',
|
||||||
|
'right' : 'arrow_right',
|
||||||
|
'home' : 'center_cursor_in_art',
|
||||||
|
'l' : 'cycle_inactive_layer_visibility',
|
||||||
|
'alt f' : 'open_file_menu',
|
||||||
|
'alt e' : 'open_edit_menu',
|
||||||
|
'alt t' : 'open_tool_menu',
|
||||||
|
'alt v' : 'open_view_menu',
|
||||||
|
'alt a' : 'open_art_menu',
|
||||||
|
'alt r' : 'open_frame_menu',
|
||||||
|
'alt l' : 'open_layer_menu',
|
||||||
|
'alt c' : 'open_char_color_menu',
|
||||||
|
'alt g' : 'open_game_menu',
|
||||||
|
'alt h' : 'open_help_menu',
|
||||||
|
'alt s' : 'open_state_menu',
|
||||||
|
'alt w' : 'open_world_menu',
|
||||||
|
'alt o' : 'open_object_menu',
|
||||||
|
'ctrl o' : 'open_art',
|
||||||
|
'ctrl n' : 'new_art',
|
||||||
|
'ctrl w' : 'close_art',
|
||||||
|
'f1' : 'open_help_docs',
|
||||||
|
'ctrl k' : 'crop_to_selection',
|
||||||
|
'ctrl r' : 'resize_art',
|
||||||
|
'ctrl t' : 'run_art_script_last',
|
||||||
|
'ctrl f' : 'add_frame',
|
||||||
|
'ctrl l' : ('add_layer', 'select_objects'),
|
||||||
|
'ctrl h' : 'choose_charset',
|
||||||
|
'ctrl p' : ('choose_palette', 'choose_spawn_object_class'),
|
||||||
|
'o' : 'toggle_onion_visibility',
|
||||||
|
'f5' : 'toggle_all_origin_viz',
|
||||||
|
'f6' : 'toggle_all_bounds_viz',
|
||||||
|
'f7' : 'toggle_all_collision_viz',
|
||||||
|
'f8' : 'toggle_debug_text',
|
||||||
|
'f9' : 'toggle_fps_counter',
|
||||||
|
'f3' : 'toggle_collision_on_selected',
|
||||||
|
'tab' : 'switch_edit_panel_focus',
|
||||||
|
'shift tab': 'switch_edit_panel_focus_reverse',
|
||||||
|
# commands that don't have a shortcut still need to be declared
|
||||||
|
# bind strings preceded by a _ will not be displayed
|
||||||
|
'_saveas' : 'save_art_as',
|
||||||
|
'_grab' : 'select_grab_tool',
|
||||||
|
'_switch_art' : 'art_switch_to',
|
||||||
|
'_switch_layer' : 'layer_switch_to',
|
||||||
|
'_layer_viz' : 'toggle_layer_visibility',
|
||||||
|
'_hidden_layers': 'toggle_hidden_layers_visible',
|
||||||
|
'_website' : 'open_website',
|
||||||
|
'_docs' : 'generate_docs',
|
||||||
|
'_dup_frame' : 'duplicate_frame',
|
||||||
|
'_frame_delay' : 'change_frame_delay',
|
||||||
|
'_frame_delay_all': 'change_frame_delay_all',
|
||||||
|
'_frame_index' : 'change_frame_index',
|
||||||
|
'_delete_frame' : 'delete_frame',
|
||||||
|
'_dup_layer' : 'duplicate_layer',
|
||||||
|
'_layer_name' : 'change_layer_name',
|
||||||
|
'_layer_z' : 'change_layer_z',
|
||||||
|
'_delete_layer' : 'delete_layer',
|
||||||
|
'_pal_from_file': 'palette_from_file',
|
||||||
|
'_cycle_onion_frames': 'cycle_onion_frames',
|
||||||
|
'_cycle_onion_display': 'cycle_onion_ahead_behind',
|
||||||
|
'_open_game_assets' : 'open_all_game_assets',
|
||||||
|
'_export_file' : 'export_file',
|
||||||
|
'_import_file': 'import_file',
|
||||||
|
'_revert' : 'revert_art',
|
||||||
|
'_new_game' : 'new_game_dir',
|
||||||
|
'_duplicate_objects': 'duplicate_selected_objects',
|
||||||
|
'_edit_world' : 'edit_world_properties',
|
||||||
|
'_save_game' : 'save_game_state',
|
||||||
|
'_change_room' : 'change_current_room',
|
||||||
|
'_change_room_to': 'change_current_room_to',
|
||||||
|
'_add_room' : 'add_room',
|
||||||
|
'_remove_room' : 'remove_current_room',
|
||||||
|
'_room_objects' : 'set_room_objects',
|
||||||
|
'_object_rooms' : 'set_object_rooms',
|
||||||
|
'_show_all_rooms': 'toggle_all_rooms_visible',
|
||||||
|
'_set_room_cam' : 'set_room_camera_marker',
|
||||||
|
'_obj_to_cam' : 'objects_to_camera',
|
||||||
|
'_cam_to_obj' : 'camera_to_objects',
|
||||||
|
'_add_to_room' : 'add_selected_to_room',
|
||||||
|
'_remove_from_room': 'remove_selected_from_room',
|
||||||
|
'_room_edge_warps': 'set_room_edge_warps',
|
||||||
|
'_room_bounds' : 'set_room_bounds_obj',
|
||||||
|
'_room_cameras' : 'toggle_room_camera_changes',
|
||||||
|
'_list_room_objs': 'toggle_list_only_room_objects',
|
||||||
|
'_rename_room' : 'rename_current_room',
|
||||||
|
'_toggle_debug_objects': 'toggle_debug_objects',
|
||||||
|
'_toggle_picker_hold': 'toggle_picker_hold',
|
||||||
|
'_set_camera_zoom': 'set_camera_zoom',
|
||||||
|
'_toggle_bg_texture': 'toggle_bg_texture',
|
||||||
|
'_run_art_script': 'run_art_script',
|
||||||
|
'_art_flip_horizontal': 'art_flip_horizontal',
|
||||||
|
'_art_flip_vertical': 'art_flip_vertical',
|
||||||
|
'_art_toggle_flip_affects_xforms': 'art_toggle_flip_affects_xforms',
|
||||||
|
'_edit_cfg': 'edit_cfg',
|
||||||
|
'_select_overlay_image': 'select_overlay_image',
|
||||||
|
'_set_overlay_image_opacity': 'set_overlay_image_opacity',
|
||||||
|
'_set_overlay_image_scaling': 'set_overlay_image_scaling',
|
||||||
|
'_toggle_art_toolbar': 'toggle_art_toolbar',
|
||||||
|
'_cycle_fill_boundary_mode': 'cycle_fill_boundary_mode'
|
||||||
|
}
|
||||||
7
build_mac.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pyinstaller playscii_mac.spec
|
||||||
|
cd dist/Playscii.app/Contents/MacOS/
|
||||||
|
ln -s libSDL2-2.0.0.dylib libSDL2.dylib
|
||||||
|
ln -s libSDL2_mixer-2.0.0.dylib libSDL2_mixer.dylib
|
||||||
|
cd ../../../../
|
||||||
|
hdiutil create -ov -size 110m -srcfolder dist/Playscii.app/ playscii_mac-`cat version`.dmg
|
||||||
|
|
||||||
37
build_windows.bat
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
set BUILD_EXE_PATH=dist\playscii.exe
|
||||||
|
set OUTPUT_DIR=dist\playscii\
|
||||||
|
set ICON_PATH=ui\playscii.ico
|
||||||
|
set XCOPY_INCLUDE=win_xcopy_include
|
||||||
|
set XCOPY_EXCLUDE=win_xcopy_exclude
|
||||||
|
set COPY_INCLUDE=win_copy_include
|
||||||
|
|
||||||
|
echo Creating new build...
|
||||||
|
|
||||||
|
REM ==== -F = everything in one file; -w = no console window; -i = path to icon
|
||||||
|
python -m PyInstaller -F -w -i %ICON_PATH% --exclude-module pdoc playscii.py
|
||||||
|
echo Build done!
|
||||||
|
|
||||||
|
REM ==== move build so that ZIP will have a subdir enclosing everything
|
||||||
|
mkdir %OUTPUT_DIR%
|
||||||
|
move %BUILD_EXE_PATH% %OUTPUT_DIR%
|
||||||
|
|
||||||
|
echo -----------
|
||||||
|
|
||||||
|
echo Copying external files...
|
||||||
|
REM ==== xcopy dirs recursively
|
||||||
|
for /f "tokens=*" %%i in (%XCOPY_INCLUDE%) DO (
|
||||||
|
echo %%i
|
||||||
|
xcopy /E/Y "%%i" "%OUTPUT_DIR%\%%i" /exclude:%XCOPY_EXCLUDE%
|
||||||
|
)
|
||||||
|
REM ==== regular copy files (non-recursively)
|
||||||
|
for /f "tokens=*" %%i in (%COPY_INCLUDE%) DO (
|
||||||
|
echo %%i
|
||||||
|
copy /Y "%%i" %OUTPUT_DIR% > NUL
|
||||||
|
)
|
||||||
|
|
||||||
|
echo -----------
|
||||||
|
echo Done!
|
||||||
|
|
||||||
|
pause
|
||||||
303
camera.py
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
import vector
|
||||||
|
|
||||||
|
def clamp(val, lowest, highest):
|
||||||
|
return min(highest, max(lowest, val))
|
||||||
|
|
||||||
|
class Camera:
|
||||||
|
|
||||||
|
# good starting values
|
||||||
|
start_x,start_y = 0,0
|
||||||
|
start_zoom = 2.5
|
||||||
|
x_tilt, y_tilt = 0, 0
|
||||||
|
# pan/zoom speed tuning
|
||||||
|
mouse_pan_rate = 10
|
||||||
|
pan_accel = 0.005
|
||||||
|
base_max_pan_speed = 0.8
|
||||||
|
pan_friction = 0.1
|
||||||
|
# min/max zoom % between which pan speed variation scales
|
||||||
|
pan_min_pct = 25.0
|
||||||
|
pan_max_pct = 200.0
|
||||||
|
# factor by which zoom level modifies pan speed
|
||||||
|
pan_zoom_increase_factor = 16
|
||||||
|
zoom_accel = 0.1
|
||||||
|
max_zoom_speed = 2.5
|
||||||
|
zoom_friction = 0.1
|
||||||
|
# kill velocity if below this
|
||||||
|
min_velocity = 0.05
|
||||||
|
# map extents
|
||||||
|
# starting values only, bounds are generated according to art size
|
||||||
|
min_x,max_x = -10, 50
|
||||||
|
min_y,max_y = -50, 10
|
||||||
|
use_bounds = True
|
||||||
|
min_zoom,max_zoom = 1, 1000
|
||||||
|
# matrices -> worldspace renderable vertex shader uniforms
|
||||||
|
fov = 90
|
||||||
|
near_z = 0.0001
|
||||||
|
far_z = 100000
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.reset()
|
||||||
|
self.max_pan_speed = self.base_max_pan_speed
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.x, self.y = self.start_x, self.start_y
|
||||||
|
self.z = self.start_zoom
|
||||||
|
# store look vectors so world/screen space conversions can refer to it
|
||||||
|
self.look_x, self.look_y, self.look_z = None,None,None
|
||||||
|
self.vel_x, self.vel_y, self.vel_z = 0,0,0
|
||||||
|
self.mouse_panned, self.moved_this_frame = False, False
|
||||||
|
# GameObject to focus on
|
||||||
|
self.focus_object = None
|
||||||
|
self.calc_projection_matrix()
|
||||||
|
self.calc_view_matrix()
|
||||||
|
|
||||||
|
def calc_projection_matrix(self):
|
||||||
|
self.projection_matrix = self.get_perspective_matrix()
|
||||||
|
|
||||||
|
def calc_view_matrix(self):
|
||||||
|
eye = vector.Vec3(self.x, self.y, self.z)
|
||||||
|
up = vector.Vec3(0, 1, 0)
|
||||||
|
target = vector.Vec3(eye.x + self.x_tilt, eye.y + self.y_tilt, 0)
|
||||||
|
# view axes
|
||||||
|
forward = (target - eye).normalize()
|
||||||
|
side = forward.cross(up).normalize()
|
||||||
|
upward = side.cross(forward)
|
||||||
|
m = [[side.x, upward.x, -forward.x, 0],
|
||||||
|
[side.y, upward.y, -forward.y, 0],
|
||||||
|
[side.z, upward.z, -forward.z, 0],
|
||||||
|
[-eye.dot(side), -eye.dot(upward), eye.dot(forward), 1]]
|
||||||
|
self.view_matrix = np.array(m, dtype=np.float32)
|
||||||
|
self.look_x, self.look_y, self.look_z = side, upward, forward
|
||||||
|
|
||||||
|
def get_perspective_matrix(self):
|
||||||
|
zmul = (-2 * self.near_z * self.far_z) / (self.far_z - self.near_z)
|
||||||
|
ymul = 1 / math.tan(self.fov * math.pi / 360)
|
||||||
|
aspect = self.app.window_width / self.app.window_height
|
||||||
|
xmul = ymul / aspect
|
||||||
|
m = [[xmul, 0, 0, 0],
|
||||||
|
[ 0, ymul, 0, 0],
|
||||||
|
[ 0, 0, -1, -1],
|
||||||
|
[ 0, 0, zmul, 0]]
|
||||||
|
return np.array(m, dtype=np.float32)
|
||||||
|
|
||||||
|
def get_ortho_matrix(self, width=None, height=None):
|
||||||
|
width, height = width or self.app.window_width, height or self.app.window_height
|
||||||
|
m = np.eye(4, 4, dtype=np.float32)
|
||||||
|
left, bottom = 0, 0
|
||||||
|
right, top = width, height
|
||||||
|
far_z, near_z = -1, 1
|
||||||
|
x = 2 / (right - left)
|
||||||
|
y = 2 / (top - bottom)
|
||||||
|
z = -2 / (self.far_z - self.near_z)
|
||||||
|
wx = -(right + left) / (right - left)
|
||||||
|
wy = -(top + bottom) / (top - bottom)
|
||||||
|
wz = -(self.far_z + self.near_z) / (self.far_z - self.near_z)
|
||||||
|
m = [[ x, 0, 0, 0],
|
||||||
|
[ 0, y, 0, 0],
|
||||||
|
[ 0, 0, z, 0],
|
||||||
|
[wx, wy, wz, 0]]
|
||||||
|
return np.array(m, dtype=np.float32)
|
||||||
|
|
||||||
|
def pan(self, dx, dy, keyboard=False):
|
||||||
|
# modify pan speed based on zoom according to a factor
|
||||||
|
m = (self.pan_zoom_increase_factor * self.z) / self.min_zoom
|
||||||
|
self.vel_x += dx * self.pan_accel * m
|
||||||
|
self.vel_y += dy * self.pan_accel * m
|
||||||
|
# for brevity, app passes in whether user appears to be keyboard editing
|
||||||
|
if keyboard:
|
||||||
|
self.app.keyboard_editing = True
|
||||||
|
|
||||||
|
def zoom(self, dz, keyboard=False, towards_cursor=False):
|
||||||
|
self.vel_z += dz * self.zoom_accel
|
||||||
|
# pan towards cursor while zooming?
|
||||||
|
if towards_cursor:
|
||||||
|
dx = self.app.cursor.x - self.x
|
||||||
|
dy = self.app.cursor.y - self.y
|
||||||
|
self.pan(dx, dy, keyboard)
|
||||||
|
if keyboard:
|
||||||
|
self.app.keyboard_editing = True
|
||||||
|
|
||||||
|
def get_current_zoom_pct(self):
|
||||||
|
"returns % of base (1:1) for current camera"
|
||||||
|
return (self.get_base_zoom() / self.z) * 100
|
||||||
|
|
||||||
|
def get_base_zoom(self):
|
||||||
|
"returns camera Z needed for 1:1 pixel zoom"
|
||||||
|
wh = self.app.window_height
|
||||||
|
ch = self.app.ui.active_art.charset.char_height
|
||||||
|
# TODO: understand why this produces correct result for 8x8 charsets
|
||||||
|
if ch == 8:
|
||||||
|
ch = 16
|
||||||
|
return wh / ch
|
||||||
|
|
||||||
|
def set_to_base_zoom(self):
|
||||||
|
self.z = self.get_base_zoom()
|
||||||
|
|
||||||
|
def zoom_proportional(self, direction):
|
||||||
|
"zooms in or out via increments of 1:1 pixel scales for active art"
|
||||||
|
if not self.app.ui.active_art:
|
||||||
|
return
|
||||||
|
self.app.ui.active_art.camera_zoomed_extents = False
|
||||||
|
base_zoom = self.get_base_zoom()
|
||||||
|
# build span of all 1:1 zoom increments
|
||||||
|
zooms = []
|
||||||
|
m = 1
|
||||||
|
while base_zoom / m > self.min_zoom:
|
||||||
|
zooms.append(base_zoom / m)
|
||||||
|
m *= 2
|
||||||
|
zooms.reverse()
|
||||||
|
m = 1
|
||||||
|
while base_zoom * m < self.max_zoom:
|
||||||
|
zooms.append(base_zoom * m)
|
||||||
|
m *= 2
|
||||||
|
# set zoom to nearest increment in direction we're heading
|
||||||
|
if direction > 0:
|
||||||
|
zooms.reverse()
|
||||||
|
for zoom in zooms:
|
||||||
|
if self.z > zoom:
|
||||||
|
self.z = zoom
|
||||||
|
break
|
||||||
|
elif direction < 0:
|
||||||
|
for zoom in zooms:
|
||||||
|
if self.z < zoom:
|
||||||
|
self.z = zoom
|
||||||
|
break
|
||||||
|
# kill all Z velocity for camera so we don't drift out of 1:1
|
||||||
|
self.vel_z = 0
|
||||||
|
|
||||||
|
def find_closest_zoom_extents(self):
|
||||||
|
def corners_on_screen():
|
||||||
|
art = self.app.ui.active_art
|
||||||
|
z = art.layers_z[-1]
|
||||||
|
x1, y1 = art.renderables[0].x, art.renderables[0].y
|
||||||
|
left, top = vector.world_to_screen_normalized(self.app, x1, y1, z)
|
||||||
|
x2 = x1 + art.width * art.quad_width
|
||||||
|
y2 = y1 - art.height * art.quad_height
|
||||||
|
right, bot = vector.world_to_screen_normalized(self.app, x2, y2, z)
|
||||||
|
#print('(%.3f, %.3f) -> (%.3f, %.3f)' % (left, top, right, bot))
|
||||||
|
# add 1 tile of UI chars to top and bottom margins
|
||||||
|
top_margin = 1 - self.app.ui.menu_bar.art.quad_height
|
||||||
|
bot_margin = -1 + self.app.ui.status_bar.art.quad_height
|
||||||
|
return left >= -1 and top <= top_margin and \
|
||||||
|
right <= 1 and bot >= bot_margin
|
||||||
|
# zoom out from minimum until all corners are visible
|
||||||
|
self.z = self.min_zoom
|
||||||
|
# recalc view matrix each move so projection stays correct
|
||||||
|
self.calc_view_matrix()
|
||||||
|
tries = 0
|
||||||
|
while not corners_on_screen() and tries < 30:
|
||||||
|
self.zoom_proportional(-1)
|
||||||
|
self.calc_view_matrix()
|
||||||
|
tries += 1
|
||||||
|
|
||||||
|
def toggle_zoom_extents(self, override=None):
|
||||||
|
art = self.app.ui.active_art
|
||||||
|
if override is not None:
|
||||||
|
art.camera_zoomed_extents = not override
|
||||||
|
if art.camera_zoomed_extents:
|
||||||
|
# restore cached position
|
||||||
|
self.x, self.y, self.z = art.non_extents_camera_x, art.non_extents_camera_y, art.non_extents_camera_z
|
||||||
|
else:
|
||||||
|
art.non_extents_camera_x, art.non_extents_camera_y, art.non_extents_camera_z = self.x, self.y, self.z
|
||||||
|
# center camera on art
|
||||||
|
self.x = (art.width * art.quad_width) / 2
|
||||||
|
self.y = -(art.height * art.quad_height) / 2
|
||||||
|
self.find_closest_zoom_extents()
|
||||||
|
# kill all camera velocity when snapping
|
||||||
|
self.vel_x, self.vel_y, self.vel_z = 0, 0, 0
|
||||||
|
art.camera_zoomed_extents = not art.camera_zoomed_extents
|
||||||
|
|
||||||
|
def window_resized(self):
|
||||||
|
self.calc_projection_matrix()
|
||||||
|
|
||||||
|
def set_zoom(self, z):
|
||||||
|
# TODO: set lerp target, clear if keyboard etc call zoom()
|
||||||
|
self.z = z
|
||||||
|
|
||||||
|
def set_loc(self, x, y, z):
|
||||||
|
self.x, self.y, self.z = x, y, (z or self.z) # z optional
|
||||||
|
|
||||||
|
def set_loc_from_obj(self, game_object):
|
||||||
|
self.set_loc(game_object.x, game_object.y, game_object.z)
|
||||||
|
|
||||||
|
def set_for_art(self, art):
|
||||||
|
# set limits
|
||||||
|
self.max_x = art.width * art.quad_width
|
||||||
|
self.min_y = -art.height * art.quad_height
|
||||||
|
# use saved pan/zoom
|
||||||
|
self.set_loc(art.camera_x, art.camera_y, art.camera_z)
|
||||||
|
|
||||||
|
def mouse_pan(self, dx, dy):
|
||||||
|
"pan view based on mouse delta"
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
return
|
||||||
|
m = ((1 * self.pan_zoom_increase_factor) * self.z) / self.min_zoom
|
||||||
|
m /= self.max_zoom
|
||||||
|
self.x -= dx / self.mouse_pan_rate * m
|
||||||
|
self.y += dy / self.mouse_pan_rate * m
|
||||||
|
self.vel_x = self.vel_y = 0
|
||||||
|
self.mouse_panned = True
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# zoom-proportional pan scale is based on art
|
||||||
|
if self.app.ui.active_art:
|
||||||
|
speed_scale = clamp(self.get_current_zoom_pct(),
|
||||||
|
self.pan_min_pct, self.pan_max_pct)
|
||||||
|
self.max_pan_speed = self.base_max_pan_speed / (speed_scale / 100)
|
||||||
|
else:
|
||||||
|
self.max_pan_speed = self.base_max_pan_speed
|
||||||
|
# remember last position to see if it changed
|
||||||
|
self.last_x, self.last_y, self.last_z = self.x, self.y, self.z
|
||||||
|
# if focus object is set, use it for X and Y transforms
|
||||||
|
if self.focus_object:
|
||||||
|
# track towards target
|
||||||
|
# TODO: revisit this for better feel later
|
||||||
|
dx, dy = self.focus_object.x - self.x, self.focus_object.y - self.y
|
||||||
|
l = math.sqrt(dx ** 2 + dy ** 2)
|
||||||
|
if l != 0 and l > 0.1:
|
||||||
|
il = 1 / l
|
||||||
|
dx *= il
|
||||||
|
dy *= il
|
||||||
|
self.x += dx * self.pan_friction
|
||||||
|
self.y += dy * self.pan_friction
|
||||||
|
else:
|
||||||
|
# clamp velocity
|
||||||
|
self.vel_x = clamp(self.vel_x, -self.max_pan_speed, self.max_pan_speed)
|
||||||
|
self.vel_y = clamp(self.vel_y, -self.max_pan_speed, self.max_pan_speed)
|
||||||
|
# apply friction
|
||||||
|
self.vel_x *= 1 - self.pan_friction
|
||||||
|
self.vel_y *= 1 - self.pan_friction
|
||||||
|
if abs(self.vel_x) < self.min_velocity:
|
||||||
|
self.vel_x = 0
|
||||||
|
if abs(self.vel_y) < self.min_velocity:
|
||||||
|
self.vel_y = 0
|
||||||
|
# if camera moves, we're not in zoom-extents state anymore
|
||||||
|
if self.app.ui.active_art and (self.vel_x or self.vel_y):
|
||||||
|
self.app.ui.active_art.camera_zoomed_extents = False
|
||||||
|
# move
|
||||||
|
self.x += self.vel_x
|
||||||
|
self.y += self.vel_y
|
||||||
|
# process Z separately
|
||||||
|
self.vel_z = clamp(self.vel_z, -self.max_zoom_speed, self.max_zoom_speed)
|
||||||
|
self.vel_z *= 1 - self.zoom_friction
|
||||||
|
if abs(self.vel_z) < self.min_velocity:
|
||||||
|
self.vel_z = 0
|
||||||
|
# as bove, if zooming turn off zoom-extents state
|
||||||
|
if self.vel_z and self.app.ui.active_art:
|
||||||
|
self.app.ui.active_art.camera_zoomed_extents = False
|
||||||
|
self.z += self.vel_z
|
||||||
|
# keep within bounds
|
||||||
|
if self.use_bounds:
|
||||||
|
self.x = clamp(self.x, self.min_x, self.max_x)
|
||||||
|
self.y = clamp(self.y, self.min_y, self.max_y)
|
||||||
|
self.z = clamp(self.z, self.min_zoom, self.max_zoom)
|
||||||
|
# set view matrix from xyz
|
||||||
|
self.calc_view_matrix()
|
||||||
|
self.moved_this_frame = self.mouse_panned or self.x != self.last_x or self.y != self.last_y or self.z != self.last_z
|
||||||
|
self.mouse_panned = False
|
||||||
|
|
||||||
|
def log_loc(self):
|
||||||
|
self.app.log('camera x=%s, y=%s, z=%s' % (self.x, self.y, self.z))
|
||||||
191
charset.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import os.path, string, time
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from texture import Texture
|
||||||
|
|
||||||
|
CHARSET_DIR = 'charsets/'
|
||||||
|
CHARSET_FILE_EXTENSION = 'char'
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterSetLord:
|
||||||
|
|
||||||
|
# time in ms between checks for hot reload
|
||||||
|
hot_reload_check_interval = 2 * 1000
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.last_check = 0
|
||||||
|
|
||||||
|
def check_hot_reload(self):
|
||||||
|
if self.app.get_elapsed_time() - self.last_check < self.hot_reload_check_interval:
|
||||||
|
return
|
||||||
|
self.last_check = self.app.get_elapsed_time()
|
||||||
|
changed = None
|
||||||
|
for charset in self.app.charsets:
|
||||||
|
if charset.has_updated():
|
||||||
|
changed = charset.filename
|
||||||
|
# reload data and image even if only one changed
|
||||||
|
try:
|
||||||
|
success = charset.load_char_data()
|
||||||
|
if success:
|
||||||
|
self.app.log('CharacterSetLord: success reloading %s' % charset.filename)
|
||||||
|
else:
|
||||||
|
self.app.log('CharacterSetLord: failed reloading %s' % charset.filename, True)
|
||||||
|
except:
|
||||||
|
self.app.log('CharacterSetLord: failed reloading %s' % charset.filename, True)
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterSet:
|
||||||
|
|
||||||
|
transparent_color = (0, 0, 0)
|
||||||
|
|
||||||
|
def __init__(self, app, src_filename, log):
|
||||||
|
self.init_success = False
|
||||||
|
self.app = app
|
||||||
|
self.filename = self.app.find_filename_path(src_filename, CHARSET_DIR,
|
||||||
|
CHARSET_FILE_EXTENSION)
|
||||||
|
if not self.filename:
|
||||||
|
self.app.log("Couldn't find character set data %s" % self.filename)
|
||||||
|
return
|
||||||
|
self.name = os.path.basename(self.filename)
|
||||||
|
self.name = os.path.splitext(self.name)[0]
|
||||||
|
# image filename discovered by character data load process
|
||||||
|
self.image_filename = None
|
||||||
|
# remember last modified times for data and image files
|
||||||
|
self.last_data_change = os.path.getmtime(self.filename)
|
||||||
|
self.last_image_change = 0
|
||||||
|
# do most stuff in load_char_data so we can hot reload
|
||||||
|
if not self.load_char_data():
|
||||||
|
return
|
||||||
|
# report
|
||||||
|
if log and not self.app.game_mode:
|
||||||
|
self.app.log("loaded charmap '%s' from %s:" % (self.name, self.filename))
|
||||||
|
self.report()
|
||||||
|
self.init_success = True
|
||||||
|
|
||||||
|
def load_char_data(self):
|
||||||
|
"carries out majority of CharacterSet init, including loading image"
|
||||||
|
char_data_src = open(self.filename, encoding='utf-8').readlines()
|
||||||
|
# allow comments: discard any line in char data starting with //
|
||||||
|
# (make sure this doesn't muck up legit mapping data)
|
||||||
|
char_data = []
|
||||||
|
for line in char_data_src:
|
||||||
|
if not line.startswith('//'):
|
||||||
|
char_data.append(line)
|
||||||
|
# first line = image file
|
||||||
|
# hold off assigning to self.image_filename til we know it's valid
|
||||||
|
img_filename = self.app.find_filename_path(char_data.pop(0).strip(), CHARSET_DIR, 'png')
|
||||||
|
if not img_filename:
|
||||||
|
self.app.log("Couldn't find character set image %s" % self.image_filename)
|
||||||
|
return False
|
||||||
|
self.image_filename = img_filename
|
||||||
|
# now that we know the image file's name, store its last modified time
|
||||||
|
self.last_image_change = os.path.getmtime(self.image_filename)
|
||||||
|
# second line = character set dimensions
|
||||||
|
second_line = char_data.pop(0).strip().split(',')
|
||||||
|
self.map_width, self.map_height = int(second_line[0]), int(second_line[1])
|
||||||
|
self.char_mapping = {}
|
||||||
|
index = 0
|
||||||
|
for line in char_data:
|
||||||
|
# strip newlines from mapping
|
||||||
|
for char in line.strip('\r\n'):
|
||||||
|
if not char in self.char_mapping:
|
||||||
|
self.char_mapping[char] = index
|
||||||
|
index += 1
|
||||||
|
if index >= self.map_width * self.map_height:
|
||||||
|
break
|
||||||
|
# if no lower case included, map upper to lower & vice versa
|
||||||
|
has_upper, has_lower = False, False
|
||||||
|
for line in char_data:
|
||||||
|
for char in line:
|
||||||
|
if char.isupper():
|
||||||
|
has_upper = True
|
||||||
|
elif char.islower():
|
||||||
|
has_lower = True
|
||||||
|
if has_upper and not has_lower:
|
||||||
|
for char in string.ascii_lowercase:
|
||||||
|
# set may not have all letters
|
||||||
|
if not char.upper() in self.char_mapping:
|
||||||
|
continue
|
||||||
|
self.char_mapping[char] = self.char_mapping[char.upper()]
|
||||||
|
elif has_lower and not has_upper:
|
||||||
|
for char in string.ascii_uppercase:
|
||||||
|
if not char.lower() in self.char_mapping:
|
||||||
|
continue
|
||||||
|
self.char_mapping[char] = self.char_mapping[char.lower()]
|
||||||
|
# last valid index a character can be
|
||||||
|
self.last_index = self.map_width * self.map_height
|
||||||
|
# load image
|
||||||
|
self.load_image_data()
|
||||||
|
self.set_char_dimensions()
|
||||||
|
# store base filename for easy comparisons with not-yet-loaded sets
|
||||||
|
self.base_filename = os.path.splitext(os.path.basename(self.filename))[0]
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load_image_data(self):
|
||||||
|
# load and process image
|
||||||
|
img = Image.open(self.image_filename)
|
||||||
|
img = img.convert('RGBA')
|
||||||
|
# flip for openGL
|
||||||
|
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||||
|
self.image_width, self.image_height = img.size
|
||||||
|
# any pixel that is "transparent color" will be made fully transparent
|
||||||
|
# any pixel that isn't will be opaque + tinted FG color
|
||||||
|
for y in range(self.image_height):
|
||||||
|
for x in range(self.image_width):
|
||||||
|
# TODO: PIL pixel access shows up in profiler, use numpy array
|
||||||
|
# assignment instead
|
||||||
|
color = img.getpixel((x, y))
|
||||||
|
if color[:3] == self.transparent_color[:3]:
|
||||||
|
# MAYBE-TODO: does keeping non-alpha color improve sampling?
|
||||||
|
img.putpixel((x, y), (color[0], color[1], color[2], 0))
|
||||||
|
self.texture = Texture(img.tobytes(), self.image_width, self.image_height)
|
||||||
|
# flip image data back and save it for later, eg image conversion
|
||||||
|
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||||
|
self.image_data = img
|
||||||
|
|
||||||
|
def set_char_dimensions(self):
|
||||||
|
# store character dimensions and UV size
|
||||||
|
self.char_width = int(self.image_width / self.map_width)
|
||||||
|
self.char_height = int(self.image_height / self.map_height)
|
||||||
|
self.u_width = self.char_width / self.image_width
|
||||||
|
self.v_height = self.char_height / self.image_height
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
self.app.log(' source texture %s is %s x %s pixels' % (self.image_filename, self.image_width, self.image_height))
|
||||||
|
self.app.log(' char pixel width/height is %s x %s' % (self.char_width, self.char_height))
|
||||||
|
self.app.log(' char map width/height is %s x %s' % (self.map_width, self.map_height))
|
||||||
|
self.app.log(' last character index: %s' % self.last_index)
|
||||||
|
|
||||||
|
def has_updated(self):
|
||||||
|
"return True if source image file has changed since last check"
|
||||||
|
# tolerate bad filenames in data, don't check stamps on nonexistent ones
|
||||||
|
if not self.image_filename or not os.path.exists(self.filename) or \
|
||||||
|
not os.path.exists(self.image_filename):
|
||||||
|
return False
|
||||||
|
data_changed = os.path.getmtime(self.filename) > self.last_data_change
|
||||||
|
img_changed = os.path.getmtime(self.image_filename) > self.last_image_change
|
||||||
|
if data_changed:
|
||||||
|
self.last_data_change = time.time()
|
||||||
|
if img_changed:
|
||||||
|
self.last_image_change = time.time()
|
||||||
|
return data_changed or img_changed
|
||||||
|
|
||||||
|
def get_char_index(self, char):
|
||||||
|
return self.char_mapping.get(char, 0)
|
||||||
|
|
||||||
|
def get_solid_pixels_in_char(self, char_index):
|
||||||
|
"Returns # of solid pixels in character at given index"
|
||||||
|
tile_x = int(char_index % self.map_width)
|
||||||
|
tile_y = int(char_index / self.map_width)
|
||||||
|
x_start = self.char_width * tile_x
|
||||||
|
x_end = x_start + self.char_width
|
||||||
|
y_start = self.char_height * tile_y
|
||||||
|
y_end = y_start + self.char_height
|
||||||
|
pixels = 0
|
||||||
|
for x in range(x_start, x_end):
|
||||||
|
for y in range(y_start, y_end):
|
||||||
|
color = self.image_data.getpixel((x, y))
|
||||||
|
if color[3] > 0:
|
||||||
|
pixels += 1
|
||||||
|
return pixels
|
||||||
9
charsets/agat.char
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Agat (Soviet Apple II clone)
|
||||||
|
agat.png
|
||||||
|
16, 6
|
||||||
|
!"#$%^'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
BIN
charsets/agat.png
Normal file
|
After Width: | Height: | Size: 953 B |
10
charsets/amiga_topaz.char
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Amiga "Topaz" workbench font
|
||||||
|
amiga_topaz.png
|
||||||
|
32, 6
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmnopqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/amiga_topaz.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
12
charsets/apple2.char
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
apple2.png
|
||||||
|
// Apple II
|
||||||
|
// from http://www.lazilong.com/apple_ii/a2font/readme.html
|
||||||
|
16, 8
|
||||||
|
!"#$%^'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/apple2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
11
charsets/atari.char
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// ATASCII (Atari ASCII)
|
||||||
|
atari.png
|
||||||
|
16, 8
|
||||||
|
|
||||||
|
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^_
|
||||||
|
abcdefghijklmno
|
||||||
|
pqrstuvwxyz |
|
||||||
BIN
charsets/atari.png
Normal file
|
After Width: | Height: | Size: 682 B |
11
charsets/atari_st.char
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Atari ST desktop font
|
||||||
|
atari_st.png
|
||||||
|
32, 8
|
||||||
|
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmnopqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/atari_st.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
18
charsets/bbc_master.char
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// BBC Micro
|
||||||
|
bbc_master.png
|
||||||
|
16, 14
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^_
|
||||||
|
£abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//end
|
||||||
BIN
charsets/bbc_master.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
16
charsets/c64_edscii.char
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Commodore 64 (EDSCII version)
|
||||||
|
// first line: source image (base filename + .png assumed)
|
||||||
|
c64_edscii.png
|
||||||
|
// second line: map dimensions
|
||||||
|
16, 10
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@abcdefghijklmno
|
||||||
|
pqrstuvwxyz[ ]
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ
|
||||||
|
_
|
||||||
|
|
||||||
|
|
||||||
|
\
|
||||||
|
// end
|
||||||
BIN
charsets/c64_edscii.png
Normal file
|
After Width: | Height: | Size: 773 B |
16
charsets/c64_petscii.char
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Commodore 64 (PETSCII editor version)
|
||||||
|
// first line: source image (base filename + .png assumed)
|
||||||
|
c64_petscii.png
|
||||||
|
// second line: map dimensions
|
||||||
|
16, 10
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[ ]
|
||||||
|
@!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
abcdefghijklmnop
|
||||||
|
qrstuvwxyz
|
||||||
|
// end
|
||||||
BIN
charsets/c64_petscii.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
12
charsets/c64_real_shifted.char
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Commodore 64 (real hardware, shifted set)
|
||||||
|
c64_real_shifted.png
|
||||||
|
16, 8
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[ ]
|
||||||
|
\
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/c64_real_shifted.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
12
charsets/c64_real_unshifted.char
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Commodore 64 (real hardware, unshifted set)
|
||||||
|
c64_real_unshifted.png
|
||||||
|
16, 8
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@abcdefghijklmno
|
||||||
|
pqrstuvwxyz[ ]
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/c64_real_unshifted.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
13
charsets/commodore_pet.char
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Commodore PET
|
||||||
|
commodore_pet.png
|
||||||
|
16, 10
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]
|
||||||
|
@!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
\
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
abcdefghijklmno
|
||||||
|
pqrstuvwxyz
|
||||||
BIN
charsets/commodore_pet.png
Normal file
|
After Width: | Height: | Size: 827 B |
17
charsets/cpc.char
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Amstrad CPC
|
||||||
|
cpc.png
|
||||||
|
16, 14
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\] _
|
||||||
|
`abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
|
^`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/cpc.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
11
charsets/dos.char
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// IBM PC code page 437 (aka "extended ASCII")
|
||||||
|
dos.png
|
||||||
|
32, 8
|
||||||
|
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmnopqrstuvwxyz{|}~
|
||||||
|
ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ
|
||||||
|
áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐
|
||||||
|
└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀
|
||||||
|
αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ
|
||||||
BIN
charsets/dos.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
19
charsets/dos40.char
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// IBM PC code page 437 (40-column version)
|
||||||
|
dos40.png
|
||||||
|
16, 16
|
||||||
|
|
||||||
|
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
|
ÇüéâäàåçêëèïîìÄÅ
|
||||||
|
ÉæÆôöòûùÿÖÜ¢£¥₧ƒ
|
||||||
|
áíóúñѪº¿⌐¬½¼¡«»
|
||||||
|
░▒▓│┤╡╢╖╕╣║╗╝╜╛┐
|
||||||
|
└┴┬├─┼╞╟╚╔╩╦╠═╬╧
|
||||||
|
╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀
|
||||||
|
αßΓπΣσµτΦΘΩδ∞φε∩
|
||||||
|
≡±≥≤⌠⌡÷≈°∙·√ⁿ
|
||||||
BIN
charsets/dos40.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
6
charsets/dos_basic.char
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// "Basic" (only keyboardable chars) ASCII
|
||||||
|
dos_basic.png
|
||||||
|
32, 3
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmnopqrstuvwxyz{|}~
|
||||||
BIN
charsets/dos_basic.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
27
charsets/intellivision.char
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Mattel Intellivision (built-in)
|
||||||
|
intellivision.png
|
||||||
|
16, 14
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]^
|
||||||
|
`abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/intellivision.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
20
charsets/jpetscii.char
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// custom, PETSCII-derived set by JP LeBreton
|
||||||
|
jpetscii.png
|
||||||
|
16, 16
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[]{}
|
||||||
|
"abcdefghijklmno
|
||||||
|
pqrstuvwxyz
|
||||||
|
1234567890-=_+:;
|
||||||
|
!@#$%^&*()</>,.?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`'
|
||||||
|
// end
|
||||||
BIN
charsets/jpetscii.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
12
charsets/msx.char
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// MSX home computers
|
||||||
|
msx.png
|
||||||
|
32, 8
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
`abcdefghijklmnopqrstuvwxyz{|}~
|
||||||
|
ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ
|
||||||
|
áíóúñѪº¿⌐¬½¼¡«»ÃãĨĩÕõŨũIJij¾∽◇‰¶§
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// end
|
||||||
BIN
charsets/msx.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
4
charsets/osamu-micro.char
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// by @lunlumoart
|
||||||
|
osamu-micro.png
|
||||||
|
24, 7
|
||||||
|
|
||||||
BIN
charsets/osamu-micro.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
9
charsets/pacman.char
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Pac-Man ROM tiles
|
||||||
|
pacman.png
|
||||||
|
16, 6
|
||||||
|
ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ!
|
||||||
|
0123456789/-"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/pacman.png
Normal file
|
After Width: | Height: | Size: 1,021 B |
26
charsets/sharp.char
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Sharp MZ-700 aka "SharpSCII"
|
||||||
|
sharp.png
|
||||||
|
16, 22
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
!"#$%&'()<>[]/\
|
||||||
|
0123456789:;*=+-
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ,.?
|
||||||
|
abcdefghijklmno
|
||||||
|
pqrstuvwxyz
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
charsets/sharp.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
10
charsets/speccy.char
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// ZX Spectrum
|
||||||
|
speccy.png
|
||||||
|
16,7
|
||||||
|
!"#$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ[\]↑_
|
||||||
|
£abcdefghijklmno
|
||||||
|
pqrstuvwxyz{|}~©
|
||||||
|
|
||||||
BIN
charsets/speccy.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
13
charsets/teletext_uk.char
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Teletext (Mullard SAA5050 character generator)
|
||||||
|
teletext_uk.png
|
||||||
|
16, 10
|
||||||
|
!"£$%&'()*+,-./
|
||||||
|
0123456789:;<=>?
|
||||||
|
@ABCDEFGHIJKLMNO
|
||||||
|
PQRSTUVWXYZ½#
|
||||||
|
─abcdefghijklmno
|
||||||
|
pqrstuvwxyz¼ ÷
|
||||||
|
|
||||||
|
▌
|
||||||
|
▐
|
||||||
|
█
|
||||||
BIN
charsets/teletext_uk.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
9
charsets/ui.char
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Playscii default UI font
|
||||||
|
ui.png
|
||||||
|
27, 6
|
||||||
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
abcdefghijklmnopqrstuvwxyz
|
||||||
|
0123456789!@#$%^&*()
|
||||||
|
`-=[]\;',./~_+{}|:"<>?
|
||||||
|
…
|
||||||
|
óćø
|
||||||
BIN
charsets/ui.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
20
charsets/ultima4.char
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Ultima IV: Quest of the Avatar tileset
|
||||||
|
ultima4.png
|
||||||
|
16, 16
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ABCDEFGHIJKLMNOP
|
||||||
|
QRSTUVWXYZ
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// end
|
||||||
BIN
charsets/ultima4.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
28
code_of_conduct.txt
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
Contributor Code of Conduct
|
||||||
|
|
||||||
|
Please note that this project is released with a Contributor Code of Conduct. By
|
||||||
|
participating in this project you agree to abide by its terms:
|
||||||
|
|
||||||
|
As contributors and maintainers of this project, we pledge to respect all people who
|
||||||
|
contribute through reporting issues, posting feature requests, updating documentation,
|
||||||
|
submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in this project a harassment-free experience for
|
||||||
|
everyone, regardless of level of experience, gender, gender identity and expression,
|
||||||
|
sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include the use of sexual language or
|
||||||
|
imagery, derogatory comments or personal attacks, trolling, public or private harassment,
|
||||||
|
insults, or other unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments,
|
||||||
|
commits, code, wiki edits, issues, and other contributions that are not aligned to this
|
||||||
|
Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed
|
||||||
|
from the project team.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||||
|
opening an issue or contacting one or more of the project maintainers.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant
|
||||||
|
(http:contributor-covenant.org), version 1.0.0, available at
|
||||||
|
http://contributor-covenant.org/version/1/0/0/
|
||||||
577
collision.py
Normal file
|
|
@ -0,0 +1,577 @@
|
||||||
|
import math
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from renderable import TileRenderable
|
||||||
|
from renderable_line import CircleCollisionRenderable, BoxCollisionRenderable, TileBoxCollisionRenderable
|
||||||
|
|
||||||
|
# collision shape types
|
||||||
|
CST_NONE = 0
|
||||||
|
"Don't use a CollisionShape"
|
||||||
|
CST_CIRCLE = 1
|
||||||
|
"Use a CircleCollisionShap"
|
||||||
|
CST_AABB = 2
|
||||||
|
"Use an AABBCollisionShape"
|
||||||
|
CST_TILE = 3
|
||||||
|
"""
|
||||||
|
Tile-based collision: generate multiple AABBCollisionShapes to approximate all
|
||||||
|
non-blank (character index 0) tiles of our GameObject's default Art's
|
||||||
|
"collision layer", whose string name is defined in GO.col_layer_name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# collision types
|
||||||
|
CT_NONE = 0
|
||||||
|
CT_PLAYER = 1
|
||||||
|
CT_GENERIC_STATIC = 2
|
||||||
|
CT_GENERIC_DYNAMIC = 3
|
||||||
|
|
||||||
|
# collision type groups, eg static and dynamic
|
||||||
|
CTG_STATIC = [CT_GENERIC_STATIC]
|
||||||
|
'"Collision type group", collections of CT_* values for more convenient checks.'
|
||||||
|
CTG_DYNAMIC = [CT_GENERIC_DYNAMIC, CT_PLAYER]
|
||||||
|
'"Collision type group", collections of CT_* values for more convenient checks.'
|
||||||
|
|
||||||
|
__pdoc__ = {}
|
||||||
|
# named tuples for collision structs that don't merit a class
|
||||||
|
Contact = namedtuple('Contact', ['overlap', 'timestamp'])
|
||||||
|
__pdoc__['Contact'] = "Represents a contact between two objects."
|
||||||
|
|
||||||
|
ShapeOverlap = namedtuple('ShapeOverlap', ['x', 'y', 'dist', 'area', 'other'])
|
||||||
|
__pdoc__['ShapeOverlap'] = "Represents a CollisionShape's overlap with another."
|
||||||
|
|
||||||
|
|
||||||
|
class CollisionShape:
|
||||||
|
"""
|
||||||
|
Abstract class for a shape that can overlap and collide with other shapes.
|
||||||
|
Shapes are part of a Collideable which in turn is part of a GameObject.
|
||||||
|
"""
|
||||||
|
def resolve_overlaps_with_shapes(self, shapes):
|
||||||
|
"Resolve this shape's overlap(s) with given list of shapes."
|
||||||
|
overlaps = []
|
||||||
|
for other in shapes:
|
||||||
|
if other is self:
|
||||||
|
continue
|
||||||
|
overlap = self.get_overlap(other)
|
||||||
|
if overlap.dist < 0:
|
||||||
|
overlaps.append(overlap)
|
||||||
|
if len(overlaps) == 0:
|
||||||
|
return
|
||||||
|
# resolve collisions in order of largest -> smallest overlap
|
||||||
|
overlaps.sort(key=lambda item: item.area, reverse=True)
|
||||||
|
for i,old_overlap in enumerate(overlaps):
|
||||||
|
# resolve first overlap without recalculating
|
||||||
|
overlap = self.get_overlap(old_overlap.other) if i > 0 else overlaps[0]
|
||||||
|
self.resolve_overlap(overlap)
|
||||||
|
|
||||||
|
def resolve_overlap(self, overlap):
|
||||||
|
"Resolve this shape's given overlap."
|
||||||
|
other = overlap.other
|
||||||
|
# tell objects they're overlapping, pass penetration vector
|
||||||
|
a_coll_b, a_started_b = self.go.overlapped(other.go, overlap)
|
||||||
|
b_coll_a, b_started_a = other.go.overlapped(self.go, overlap)
|
||||||
|
# if either object says it shouldn't collide with other, don't
|
||||||
|
if not a_coll_b or not b_coll_a:
|
||||||
|
return
|
||||||
|
# push shapes apart according to mass
|
||||||
|
total_mass = max(0, self.go.mass) + max(0, other.go.mass)
|
||||||
|
if self.go.is_dynamic():
|
||||||
|
if not other.go.is_dynamic() or other.go.mass < 0:
|
||||||
|
a_push = overlap.dist
|
||||||
|
else:
|
||||||
|
a_push = (self.go.mass / total_mass) * overlap.dist
|
||||||
|
# move parent object, not shape
|
||||||
|
self.go.x += a_push * overlap.x
|
||||||
|
self.go.y += a_push * overlap.y
|
||||||
|
# update all shapes based on object's new position
|
||||||
|
self.go.collision.update_transform_from_object()
|
||||||
|
if other.go.is_dynamic():
|
||||||
|
if not self.go.is_dynamic() or self.go.mass < 0:
|
||||||
|
b_push = overlap.dist
|
||||||
|
else:
|
||||||
|
b_push = (other.go.mass / total_mass) * overlap.dist
|
||||||
|
other.go.x -= b_push * overlap.x
|
||||||
|
other.go.y -= b_push * overlap.y
|
||||||
|
other.go.collision.update_transform_from_object()
|
||||||
|
# call objs' started_colliding once collisions have been resolved
|
||||||
|
world = self.go.world
|
||||||
|
if a_started_b:
|
||||||
|
world.try_object_method(self.go, self.go.started_colliding, [other.go])
|
||||||
|
if b_started_a:
|
||||||
|
world.try_object_method(other.go, other.go.started_colliding, [self.go])
|
||||||
|
|
||||||
|
def get_overlapping_static_shapes(self):
|
||||||
|
"Return a list of static shapes that overlap with this shape."
|
||||||
|
overlapping_shapes = []
|
||||||
|
shape_left, shape_top, shape_right, shape_bottom = self.get_box()
|
||||||
|
# add padding to overlapping tiles check
|
||||||
|
if False:
|
||||||
|
padding = 0.01
|
||||||
|
shape_left -= padding
|
||||||
|
shape_top -= padding
|
||||||
|
shape_right += padding
|
||||||
|
shape_bottom += padding
|
||||||
|
for obj in self.go.world.objects.values():
|
||||||
|
if obj is self.go or not obj.should_collide() or obj.is_dynamic():
|
||||||
|
continue
|
||||||
|
# always check non-tile-based static shapes
|
||||||
|
if obj.collision_shape_type != CST_TILE:
|
||||||
|
overlapping_shapes += obj.collision.shapes
|
||||||
|
else:
|
||||||
|
# skip if even bounds don't overlap
|
||||||
|
obj_left, obj_top, obj_right, obj_bottom = obj.get_edges()
|
||||||
|
if not boxes_overlap(shape_left, shape_top, shape_right, shape_bottom,
|
||||||
|
obj_left, obj_top, obj_right, obj_bottom):
|
||||||
|
continue
|
||||||
|
overlapping_shapes += obj.collision.get_shapes_overlapping_box(shape_left, shape_top, shape_right, shape_bottom)
|
||||||
|
return overlapping_shapes
|
||||||
|
|
||||||
|
|
||||||
|
class CircleCollisionShape(CollisionShape):
|
||||||
|
"CollisionShape using a circle area."
|
||||||
|
def __init__(self, loc_x, loc_y, radius, game_object):
|
||||||
|
self.x, self.y = loc_x, loc_y
|
||||||
|
self.radius = radius
|
||||||
|
self.go = game_object
|
||||||
|
|
||||||
|
def get_box(self):
|
||||||
|
"Return world coordinates of our bounds (left, top, right, bottom)"
|
||||||
|
return self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius
|
||||||
|
|
||||||
|
def is_point_inside(self, x, y):
|
||||||
|
"Return True if given point is inside this shape."
|
||||||
|
return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius ** 2
|
||||||
|
|
||||||
|
def overlaps_line(self, x1, y1, x2, y2):
|
||||||
|
"Return True if this circle overlaps given line segment."
|
||||||
|
return circle_overlaps_line(self.x, self.y, self.radius, x1, y1, x2, y2)
|
||||||
|
|
||||||
|
def get_overlap(self, other):
|
||||||
|
"Return ShapeOverlap data for this shape's overlap with given other."
|
||||||
|
if type(other) is CircleCollisionShape:
|
||||||
|
px, py, pdist1, pdist2 = point_circle_penetration(self.x, self.y,
|
||||||
|
other.x, other.y,
|
||||||
|
self.radius + other.radius)
|
||||||
|
elif type(other) is AABBCollisionShape:
|
||||||
|
px, py, pdist1, pdist2 = circle_box_penetration(self.x, self.y,
|
||||||
|
other.x, other.y,
|
||||||
|
self.radius, other.halfwidth,
|
||||||
|
other.halfheight)
|
||||||
|
area = abs(pdist1 * pdist2) if pdist1 < 0 else 0
|
||||||
|
return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
|
||||||
|
|
||||||
|
|
||||||
|
class AABBCollisionShape(CollisionShape):
|
||||||
|
"CollisionShape using an axis-aligned bounding box area."
|
||||||
|
def __init__(self, loc_x, loc_y, halfwidth, halfheight, game_object):
|
||||||
|
self.x, self.y = loc_x, loc_y
|
||||||
|
self.halfwidth, self.halfheight = halfwidth, halfheight
|
||||||
|
self.go = game_object
|
||||||
|
# for CST_TILE objects, lists of tile(s) we cover
|
||||||
|
self.tiles = []
|
||||||
|
|
||||||
|
def get_box(self):
|
||||||
|
return self.x - self.halfwidth, self.y - self.halfheight, self.x + self.halfwidth, self.y + self.halfheight
|
||||||
|
|
||||||
|
def is_point_inside(self, x, y):
|
||||||
|
"Return True if given point is inside this shape."
|
||||||
|
return point_in_box(x, y, *self.get_box())
|
||||||
|
|
||||||
|
def overlaps_line(self, x1, y1, x2, y2):
|
||||||
|
"Return True if this box overlaps given line segment."
|
||||||
|
left, top, right, bottom = self.get_box()
|
||||||
|
return box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2)
|
||||||
|
|
||||||
|
def get_overlap(self, other):
|
||||||
|
"Return ShapeOverlap data for this shape's overlap with given other."
|
||||||
|
if type(other) is AABBCollisionShape:
|
||||||
|
px, py, pdist1, pdist2 = box_penetration(self.x, self.y,
|
||||||
|
other.x, other.y,
|
||||||
|
self.halfwidth, self.halfheight,
|
||||||
|
other.halfwidth, other.halfheight)
|
||||||
|
elif type(other) is CircleCollisionShape:
|
||||||
|
px, py, pdist1, pdist2 = circle_box_penetration(other.x, other.y,
|
||||||
|
self.x, self.y,
|
||||||
|
other.radius, self.halfwidth,
|
||||||
|
self.halfheight)
|
||||||
|
# reverse result if we're shape B
|
||||||
|
px, py = -px, -py
|
||||||
|
area = abs(pdist1 * pdist2) if pdist1 < 0 else 0
|
||||||
|
return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
|
||||||
|
|
||||||
|
|
||||||
|
class Collideable:
|
||||||
|
"Collision component for GameObjects. Contains a list of shapes."
|
||||||
|
use_art_offset = False
|
||||||
|
"use game object's art_off_pct values"
|
||||||
|
def __init__(self, obj):
|
||||||
|
"Create new Collideable for given GameObject."
|
||||||
|
self.go = obj
|
||||||
|
self.cl = self.go.world.cl
|
||||||
|
self.renderables, self.shapes = [], []
|
||||||
|
self.tile_shapes = {}
|
||||||
|
"Dict of shapes accessible by (x,y) tile coordinates"
|
||||||
|
self.contacts = {}
|
||||||
|
"Dict of contacts with other objects, by object name"
|
||||||
|
self.create_shapes()
|
||||||
|
|
||||||
|
def create_shapes(self):
|
||||||
|
"""
|
||||||
|
Create collision shape(s) appropriate to our game object's
|
||||||
|
collision_shape_type value.
|
||||||
|
"""
|
||||||
|
self._clear_shapes()
|
||||||
|
if self.go.collision_shape_type == CST_NONE:
|
||||||
|
return
|
||||||
|
elif self.go.collision_shape_type == CST_CIRCLE:
|
||||||
|
self._create_circle()
|
||||||
|
elif self.go.collision_shape_type == CST_AABB:
|
||||||
|
self._create_box()
|
||||||
|
elif self.go.collision_shape_type == CST_TILE:
|
||||||
|
self.tile_shapes.clear()
|
||||||
|
self._create_merged_tile_boxes()
|
||||||
|
# update renderables once if static
|
||||||
|
if not self.go.is_dynamic():
|
||||||
|
self.update_renderables()
|
||||||
|
|
||||||
|
def _clear_shapes(self):
|
||||||
|
for r in self.renderables:
|
||||||
|
r.destroy()
|
||||||
|
self.renderables = []
|
||||||
|
for shape in self.shapes:
|
||||||
|
self.cl._remove_shape(shape)
|
||||||
|
self.shapes = []
|
||||||
|
"List of CollisionShapes"
|
||||||
|
|
||||||
|
def _create_circle(self):
|
||||||
|
x = self.go.x + self.go.col_offset_x
|
||||||
|
y = self.go.y + self.go.col_offset_y
|
||||||
|
shape = self.cl._add_circle_shape(x, y, self.go.col_radius, self.go)
|
||||||
|
self.shapes = [shape]
|
||||||
|
self.renderables = [CircleCollisionRenderable(shape)]
|
||||||
|
|
||||||
|
def _create_box(self):
|
||||||
|
x = self.go.x # + self.go.col_offset_x
|
||||||
|
y = self.go.y # + self.go.col_offset_y
|
||||||
|
shape = self.cl._add_box_shape(x, y,
|
||||||
|
self.go.col_width / 2,
|
||||||
|
self.go.col_height / 2,
|
||||||
|
self.go)
|
||||||
|
self.shapes = [shape]
|
||||||
|
self.renderables = [BoxCollisionRenderable(shape)]
|
||||||
|
|
||||||
|
def _create_merged_tile_boxes(self):
|
||||||
|
"Create AABB shapes for a CST_TILE object"
|
||||||
|
# generate fewer, larger boxes!
|
||||||
|
frame = self.go.renderable.frame
|
||||||
|
if not self.go.col_layer_name in self.go.art.layer_names:
|
||||||
|
self.go.app.dev_log("%s: Couldn't find collision layer with name '%s'" % (self.go.name, self.go.col_layer_name))
|
||||||
|
return
|
||||||
|
layer = self.go.art.layer_names.index(self.go.col_layer_name)
|
||||||
|
# tile is available if it's not empty and not already covered by a shape
|
||||||
|
def tile_available(tile_x, tile_y):
|
||||||
|
return self.go.art.get_char_index_at(frame, layer, tile_x, tile_y) != 0 and not (tile_x, tile_y) in self.tile_shapes
|
||||||
|
def tile_range_available(start_x, end_x, start_y, end_y):
|
||||||
|
for y in range(start_y, end_y + 1):
|
||||||
|
for x in range(start_x, end_x + 1):
|
||||||
|
if not tile_available(x, y):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
for y in range(self.go.art.height):
|
||||||
|
for x in range(self.go.art.width):
|
||||||
|
if not tile_available(x, y):
|
||||||
|
continue
|
||||||
|
# determine how big we can make this box
|
||||||
|
# first fill left to right
|
||||||
|
end_x = x
|
||||||
|
while end_x < self.go.art.width - 1 and tile_available(end_x + 1, y):
|
||||||
|
end_x += 1
|
||||||
|
# then fill top to bottom
|
||||||
|
end_y = y
|
||||||
|
while end_y < self.go.art.height - 1 and tile_range_available(x, end_x, y, end_y + 1):
|
||||||
|
end_y += 1
|
||||||
|
# compute origin and halfsizes of box covering tile range
|
||||||
|
wx1, wy1 = self.go.get_tile_loc(x, y, tile_center=True)
|
||||||
|
wx2, wy2 = self.go.get_tile_loc(end_x, end_y, tile_center=True)
|
||||||
|
wx = (wx1 + wx2) / 2
|
||||||
|
halfwidth = (end_x - x) * self.go.art.quad_width
|
||||||
|
halfwidth /= 2
|
||||||
|
halfwidth += self.go.art.quad_width / 2
|
||||||
|
wy = (wy1 + wy2) / 2
|
||||||
|
halfheight = (end_y - y) * self.go.art.quad_height
|
||||||
|
halfheight /= 2
|
||||||
|
halfheight += self.go.art.quad_height / 2
|
||||||
|
shape = self.cl._add_box_shape(wx, wy, halfwidth, halfheight,
|
||||||
|
self.go)
|
||||||
|
# fill in cell(s) in our tile collision dict,
|
||||||
|
# write list of tiles shape covers to shape.tiles
|
||||||
|
for tile_y in range(y, end_y + 1):
|
||||||
|
for tile_x in range(x, end_x + 1):
|
||||||
|
self.tile_shapes[(tile_x, tile_y)] = shape
|
||||||
|
shape.tiles.append((tile_x, tile_y))
|
||||||
|
r = TileBoxCollisionRenderable(shape)
|
||||||
|
# update renderable once to set location correctly
|
||||||
|
r.update()
|
||||||
|
self.shapes.append(shape)
|
||||||
|
self.renderables.append(r)
|
||||||
|
|
||||||
|
def get_shape_overlapping_point(self, x, y):
|
||||||
|
"Return shape if it's overlapping given point, None if no overlap."
|
||||||
|
tile_x, tile_y = self.go.get_tile_at_point(x, y)
|
||||||
|
return self.tile_shapes.get((tile_x, tile_y), None)
|
||||||
|
|
||||||
|
def get_shapes_overlapping_box(self, left, top, right, bottom):
|
||||||
|
"Return a list of our shapes that overlap given box."
|
||||||
|
shapes = []
|
||||||
|
tiles = self.go.get_tiles_overlapping_box(left, top, right, bottom)
|
||||||
|
for (x, y) in tiles:
|
||||||
|
shape = self.tile_shapes.get((x, y), None)
|
||||||
|
if shape and not shape in shapes:
|
||||||
|
shapes.append(shape)
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.go and self.go.is_dynamic():
|
||||||
|
self.update_transform_from_object()
|
||||||
|
|
||||||
|
def update_transform_from_object(self, obj=None):
|
||||||
|
"Snap our shapes to location of given object (if unspecified, our GO)."
|
||||||
|
obj = obj or self.go
|
||||||
|
# CST_TILE shouldn't run here, it's static-only
|
||||||
|
if obj.collision_shape_type == CST_TILE:
|
||||||
|
return
|
||||||
|
for shape in self.shapes:
|
||||||
|
shape.x = obj.x + obj.col_offset_x
|
||||||
|
shape.y = obj.y + obj.col_offset_y
|
||||||
|
|
||||||
|
def set_shape_color(self, shape, new_color):
|
||||||
|
"Set the color of a given shape's debug LineRenderable."
|
||||||
|
try:
|
||||||
|
shape_index = self.shapes.index(shape)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
self.renderables[shape_index].color = new_color
|
||||||
|
self.renderables[shape_index].build_geo()
|
||||||
|
self.renderables[shape_index].rebind_buffers()
|
||||||
|
|
||||||
|
def update_renderables(self):
|
||||||
|
for r in self.renderables:
|
||||||
|
r.update()
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
for r in self.renderables:
|
||||||
|
r.render()
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
for r in self.renderables:
|
||||||
|
r.destroy()
|
||||||
|
# remove our shapes from CollisionLord's shape list
|
||||||
|
for shape in self.shapes:
|
||||||
|
self.cl._remove_shape(shape)
|
||||||
|
|
||||||
|
|
||||||
|
class CollisionLord:
|
||||||
|
"""
|
||||||
|
Collision manager object, tracks Collideables, detects overlaps and
|
||||||
|
resolves collisions.
|
||||||
|
"""
|
||||||
|
iterations = 7
|
||||||
|
"""
|
||||||
|
Number of times to resolve collisions per update. Lower at own risk;
|
||||||
|
multi-object collisions require multiple iterations to settle correctly.
|
||||||
|
"""
|
||||||
|
def __init__(self, world):
|
||||||
|
self.world = world
|
||||||
|
self.ticks = 0
|
||||||
|
# list of objects processed for collision this frame
|
||||||
|
self.collisions_this_frame = []
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
print('%s: %s dynamic shapes, %s static shapes' % (self,
|
||||||
|
len(self.dynamic_shapes),
|
||||||
|
len(self.static_shapes)))
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.dynamic_shapes, self.static_shapes = [], []
|
||||||
|
|
||||||
|
def _add_circle_shape(self, x, y, radius, game_object):
|
||||||
|
shape = CircleCollisionShape(x, y, radius, game_object)
|
||||||
|
if game_object.is_dynamic():
|
||||||
|
self.dynamic_shapes.append(shape)
|
||||||
|
else:
|
||||||
|
self.static_shapes.append(shape)
|
||||||
|
return shape
|
||||||
|
|
||||||
|
def _add_box_shape(self, x, y, halfwidth, halfheight, game_object):
|
||||||
|
shape = AABBCollisionShape(x, y, halfwidth, halfheight, game_object)
|
||||||
|
if game_object.is_dynamic():
|
||||||
|
self.dynamic_shapes.append(shape)
|
||||||
|
else:
|
||||||
|
self.static_shapes.append(shape)
|
||||||
|
return shape
|
||||||
|
|
||||||
|
def _remove_shape(self, shape):
|
||||||
|
if shape in self.dynamic_shapes:
|
||||||
|
self.dynamic_shapes.remove(shape)
|
||||||
|
elif shape in self.static_shapes:
|
||||||
|
self.static_shapes.remove(shape)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"Resolve overlaps between all relevant world objects."
|
||||||
|
for i in range(self.iterations):
|
||||||
|
# filter shape lists for anything out of room etc
|
||||||
|
valid_dynamic_shapes = []
|
||||||
|
for shape in self.dynamic_shapes:
|
||||||
|
if shape.go.should_collide():
|
||||||
|
valid_dynamic_shapes.append(shape)
|
||||||
|
for shape in valid_dynamic_shapes:
|
||||||
|
shape.resolve_overlaps_with_shapes(valid_dynamic_shapes)
|
||||||
|
for shape in valid_dynamic_shapes:
|
||||||
|
static_shapes = shape.get_overlapping_static_shapes()
|
||||||
|
shape.resolve_overlaps_with_shapes(static_shapes)
|
||||||
|
# check which objects stopped colliding
|
||||||
|
for obj in self.world.objects.values():
|
||||||
|
obj.check_finished_contacts()
|
||||||
|
self.ticks += 1
|
||||||
|
self.collisions_this_frame = []
|
||||||
|
|
||||||
|
|
||||||
|
# collision handling
|
||||||
|
|
||||||
|
def point_in_box(x, y, box_left, box_top, box_right, box_bottom):
|
||||||
|
"Return True if given point lies within box with given corners."
|
||||||
|
return box_left <= x <= box_right and box_bottom <= y <= box_top
|
||||||
|
|
||||||
|
def boxes_overlap(left_a, top_a, right_a, bottom_a,
|
||||||
|
left_b, top_b, right_b, bottom_b):
|
||||||
|
"Return True if given boxes A and B overlap."
|
||||||
|
for (x, y) in ((left_a, top_a), (right_a, top_a),
|
||||||
|
(right_a, bottom_a), (left_a, bottom_a)):
|
||||||
|
if left_b <= x <= right_b and bottom_b <= y <= top_b:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
|
||||||
|
"Return True if given lines intersect."
|
||||||
|
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||||
|
numer = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
|
||||||
|
numer2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
|
||||||
|
if denom == 0:
|
||||||
|
if numer == 0 and numer2 == 0:
|
||||||
|
# coincident
|
||||||
|
return False
|
||||||
|
# parallel
|
||||||
|
return False
|
||||||
|
ua = numer / denom
|
||||||
|
ub = numer2 / denom
|
||||||
|
return ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1
|
||||||
|
|
||||||
|
def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2):
|
||||||
|
"Return point on given line that's closest to given point."
|
||||||
|
wx, wy = point_x - x1, point_y - y1
|
||||||
|
dir_x, dir_y = x2 - x1, y2 - y1
|
||||||
|
proj = wx * dir_x + wy * dir_y
|
||||||
|
if proj <= 0:
|
||||||
|
# line point 1 is closest
|
||||||
|
return x1, y1
|
||||||
|
vsq = dir_x ** 2 + dir_y ** 2
|
||||||
|
if proj >= vsq:
|
||||||
|
# line point 2 is closest
|
||||||
|
return x2, y2
|
||||||
|
else:
|
||||||
|
# closest point is between 1 and 2
|
||||||
|
return x1 + (proj / vsq) * dir_x, y1 + (proj / vsq) * dir_y
|
||||||
|
|
||||||
|
def circle_overlaps_line(circle_x, circle_y, radius, x1, y1, x2, y2):
|
||||||
|
"Return True if given circle overlaps given line."
|
||||||
|
# get closest point on line to circle center
|
||||||
|
closest_x, closest_y = line_point_closest_to_point(circle_x, circle_y,
|
||||||
|
x1, y1, x2, y2)
|
||||||
|
dist_x, dist_y = closest_x - circle_x, closest_y - circle_y
|
||||||
|
return dist_x ** 2 + dist_y ** 2 <= radius ** 2
|
||||||
|
|
||||||
|
def box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2):
|
||||||
|
"Return True if given box overlaps given line."
|
||||||
|
# TODO: determine if this is less efficient than slab method below
|
||||||
|
if point_in_box(x1, y1, left, top, right, bottom) and \
|
||||||
|
point_in_box(x2, y2, left, top, right, bottom):
|
||||||
|
return True
|
||||||
|
# check left/top/right/bottoms edges
|
||||||
|
return lines_intersect(left, top, left, bottom, x1, y1, x2, y2) or \
|
||||||
|
lines_intersect(left, top, right, top, x1, y1, x2, y2) or \
|
||||||
|
lines_intersect(right, top, right, bottom, x1, y1, x2, y2) or \
|
||||||
|
lines_intersect(left, bottom, right, bottom, x1, y1, x2, y2)
|
||||||
|
|
||||||
|
def box_overlaps_ray(left, top, right, bottom, x1, y1, x2, y2):
|
||||||
|
"Return True if given box overlaps given ray."
|
||||||
|
# TODO: determine if this can be adapted for line segments
|
||||||
|
# (just a matter of setting tmin/tmax properly?)
|
||||||
|
tmin, tmax = -math.inf, math.inf
|
||||||
|
dir_x, dir_y = x2 - x1, y2 - y1
|
||||||
|
if abs(dir_x) > 0:
|
||||||
|
tx1 = (left - x1) / dir_x
|
||||||
|
tx2 = (right - x1) / dir_x
|
||||||
|
tmin = max(tmin, min(tx1, tx2))
|
||||||
|
tmax = min(tmax, max(tx1, tx2))
|
||||||
|
if abs(dir_y) > 0:
|
||||||
|
ty1 = (top - y1) / dir_y
|
||||||
|
ty2 = (bottom - y1) / dir_y
|
||||||
|
tmin = max(tmin, min(ty1, ty2))
|
||||||
|
tmax = min(tmax, max(ty1, ty2))
|
||||||
|
return tmax >= tmin
|
||||||
|
|
||||||
|
def point_circle_penetration(point_x, point_y, circle_x, circle_y, radius):
|
||||||
|
"Return normalized penetration x, y, and distance for given circles."
|
||||||
|
dx, dy = circle_x - point_x, circle_y - point_y
|
||||||
|
pdist = math.sqrt(dx ** 2 + dy ** 2)
|
||||||
|
# point is center of circle, arbitrarily project out in +X
|
||||||
|
if pdist == 0:
|
||||||
|
return 1, 0, -radius, -radius
|
||||||
|
# TODO: calculate other axis of intersection for area?
|
||||||
|
return dx / pdist, dy / pdist, pdist - radius, pdist - radius
|
||||||
|
|
||||||
|
def box_penetration(ax, ay, bx, by, ahw, ahh, bhw, bhh):
|
||||||
|
"Return penetration vector and magnitude for given boxes."
|
||||||
|
left_a, right_a = ax - ahw, ax + ahw
|
||||||
|
top_a, bottom_a = ay + ahh, ay - ahh
|
||||||
|
left_b, right_b = bx - bhw, bx + bhw
|
||||||
|
top_b, bottom_b = by + bhh, by - bhh
|
||||||
|
# A to left or right of B?
|
||||||
|
px = right_a - left_b if ax <= bx else right_b - left_a
|
||||||
|
# A above or below B?
|
||||||
|
py = top_b - bottom_a if ay >= by else top_a - bottom_b
|
||||||
|
dx, dy = bx - ax, by - ay
|
||||||
|
widths, heights = ahw + bhw, ahh + bhh
|
||||||
|
# return separating axis + penetration depth (+ other axis for area calc)
|
||||||
|
if widths + px - abs(dx) < heights + py - abs(dy):
|
||||||
|
if dx >= 0:
|
||||||
|
return 1, 0, -px, -py
|
||||||
|
elif dx < 0:
|
||||||
|
return -1, 0, -px, -py
|
||||||
|
else:
|
||||||
|
if dy >= 0:
|
||||||
|
return 0, 1, -py, -px
|
||||||
|
elif dy < 0:
|
||||||
|
return 0, -1, -py, -px
|
||||||
|
|
||||||
|
def circle_box_penetration(circle_x, circle_y, box_x, box_y, circle_radius,
|
||||||
|
box_hw, box_hh):
|
||||||
|
"Return penetration vector and magnitude for given circle and box."
|
||||||
|
box_left, box_right = box_x - box_hw, box_x + box_hw
|
||||||
|
box_top, box_bottom = box_y + box_hh, box_y - box_hh
|
||||||
|
# if circle center inside box, use box-on-box penetration vector + distance
|
||||||
|
if point_in_box(circle_x, circle_y, box_left, box_top, box_right, box_bottom):
|
||||||
|
return box_penetration(circle_x, circle_y, box_x, box_y,
|
||||||
|
circle_radius, circle_radius, box_hw, box_hh)
|
||||||
|
# find point on AABB edges closest to center of circle
|
||||||
|
# clamp = min(highest, max(lowest, val))
|
||||||
|
px = min(box_right, max(box_left, circle_x))
|
||||||
|
py = min(box_top, max(box_bottom, circle_y))
|
||||||
|
closest_x = circle_x - px
|
||||||
|
closest_y = circle_y - py
|
||||||
|
d = math.sqrt(closest_x ** 2 + closest_y ** 2)
|
||||||
|
pdist = circle_radius - d
|
||||||
|
if d == 0:
|
||||||
|
return
|
||||||
|
1, 0, -pdist, -pdist
|
||||||
|
# TODO: calculate other axis of intersection for area?
|
||||||
|
return -closest_x / d, -closest_y / d, -pdist, -pdist
|
||||||
376
cursor.py
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
import math, ctypes
|
||||||
|
import numpy as np
|
||||||
|
from OpenGL import GL
|
||||||
|
|
||||||
|
import vector
|
||||||
|
from edit_command import EditCommand
|
||||||
|
from renderable_sprite import UISpriteRenderable
|
||||||
|
|
||||||
|
"""
|
||||||
|
reference diagram:
|
||||||
|
|
||||||
|
0 0.2 0.8 1.0
|
||||||
|
A--------B *--------*
|
||||||
|
| | | |
|
||||||
|
0.1 | D-----C *-----* |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
0.2 F--E *--*
|
||||||
|
|
||||||
|
etc
|
||||||
|
"""
|
||||||
|
|
||||||
|
OUTSIDE_EDGE_SIZE = 0.2
|
||||||
|
THICKNESS = 0.1
|
||||||
|
|
||||||
|
corner_verts = [
|
||||||
|
0, 0, # A/0
|
||||||
|
OUTSIDE_EDGE_SIZE, 0, # B/1
|
||||||
|
OUTSIDE_EDGE_SIZE, -THICKNESS, # C/2
|
||||||
|
THICKNESS, -THICKNESS, # D/3
|
||||||
|
THICKNESS, -OUTSIDE_EDGE_SIZE, # E/4
|
||||||
|
0, -OUTSIDE_EDGE_SIZE # F/5
|
||||||
|
]
|
||||||
|
|
||||||
|
# vert indices for the above
|
||||||
|
corner_elems = [
|
||||||
|
0, 1, 2,
|
||||||
|
0, 2, 3,
|
||||||
|
0, 3, 4,
|
||||||
|
0, 5, 4
|
||||||
|
]
|
||||||
|
|
||||||
|
# X/Y flip transforms to make all 4 corners
|
||||||
|
# (top left, top right, bottom left, bottom right)
|
||||||
|
corner_transforms = [
|
||||||
|
( 1, 1),
|
||||||
|
(-1, 1),
|
||||||
|
( 1, -1),
|
||||||
|
(-1, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# offsets to translate the 4 corners by
|
||||||
|
corner_offsets = [
|
||||||
|
(0, 0),
|
||||||
|
(1, 0),
|
||||||
|
(0, -1),
|
||||||
|
(1, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
BASE_COLOR = (0.8, 0.8, 0.8, 1)
|
||||||
|
|
||||||
|
# why do we use the weird transforms and offsets?
|
||||||
|
# because a static vertex list wouldn't be able to adjust to different
|
||||||
|
# character set aspect ratios.
|
||||||
|
|
||||||
|
class Cursor:
|
||||||
|
|
||||||
|
vert_shader_source = 'cursor_v.glsl'
|
||||||
|
frag_shader_source = 'cursor_f.glsl'
|
||||||
|
alpha = 1
|
||||||
|
icon_scale_factor = 4
|
||||||
|
logg = False
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.x, self.y, self.z = 0, 0, 0
|
||||||
|
self.last_x, self.last_y = 0, 0
|
||||||
|
self.scale_x, self.scale_y, self.scale_z = 1, 1, 1
|
||||||
|
# list of EditCommandTiles for preview
|
||||||
|
self.preview_edits = []
|
||||||
|
self.current_command = None
|
||||||
|
# offsets to render the 4 corners at
|
||||||
|
self.mouse_x, self.mouse_y = 0, 0
|
||||||
|
self.moved = False
|
||||||
|
self.color = np.array(BASE_COLOR, dtype=np.float32)
|
||||||
|
# GL objects
|
||||||
|
if self.app.use_vao:
|
||||||
|
self.vao = GL.glGenVertexArrays(1)
|
||||||
|
GL.glBindVertexArray(self.vao)
|
||||||
|
self.vert_buffer, self.elem_buffer = GL.glGenBuffers(2)
|
||||||
|
self.vert_array = np.array(corner_verts, dtype=np.float32)
|
||||||
|
self.elem_array = np.array(corner_elems, dtype=np.uint32)
|
||||||
|
self.vert_count = int(len(self.elem_array))
|
||||||
|
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||||
|
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vert_array.nbytes,
|
||||||
|
self.vert_array, GL.GL_STATIC_DRAW)
|
||||||
|
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||||
|
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_array.nbytes,
|
||||||
|
self.elem_array, GL.GL_STATIC_DRAW)
|
||||||
|
# shader, attributes
|
||||||
|
self.shader = self.app.sl.new_shader(self.vert_shader_source, self.frag_shader_source)
|
||||||
|
# vert positions
|
||||||
|
self.pos_attrib = self.shader.get_attrib_location('vertPosition')
|
||||||
|
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||||
|
offset = ctypes.c_void_p(0)
|
||||||
|
GL.glVertexAttribPointer(self.pos_attrib, 2,
|
||||||
|
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||||
|
# uniforms
|
||||||
|
self.proj_matrix_uniform = self.shader.get_uniform_location('projection')
|
||||||
|
self.view_matrix_uniform = self.shader.get_uniform_location('view')
|
||||||
|
self.position_uniform = self.shader.get_uniform_location('objectPosition')
|
||||||
|
self.scale_uniform = self.shader.get_uniform_location('objectScale')
|
||||||
|
self.color_uniform = self.shader.get_uniform_location('baseColor')
|
||||||
|
self.quad_size_uniform = self.shader.get_uniform_location('quadSize')
|
||||||
|
self.xform_uniform = self.shader.get_uniform_location('vertTransform')
|
||||||
|
self.offset_uniform = self.shader.get_uniform_location('vertOffset')
|
||||||
|
self.alpha_uniform = self.shader.get_uniform_location('baseAlpha')
|
||||||
|
# finish
|
||||||
|
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||||
|
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||||
|
if self.app.use_vao:
|
||||||
|
GL.glBindVertexArray(0)
|
||||||
|
# init tool sprite, tool will provide texture when rendered
|
||||||
|
self.tool_sprite = UISpriteRenderable(self.app)
|
||||||
|
|
||||||
|
def clamp_to_active_art(self):
|
||||||
|
self.x = max(0, min(self.x, self.app.ui.active_art.width - 1))
|
||||||
|
self.y = min(0, max(self.y, -self.app.ui.active_art.height + 1))
|
||||||
|
|
||||||
|
def keyboard_move(self, delta_x, delta_y):
|
||||||
|
if not self.app.ui.active_art:
|
||||||
|
return
|
||||||
|
self.x += delta_x
|
||||||
|
self.y += delta_y
|
||||||
|
self.clamp_to_active_art()
|
||||||
|
self.moved = True
|
||||||
|
self.app.keyboard_editing = True
|
||||||
|
if self.logg:
|
||||||
|
self.app.log('Cursor: %s,%s,%s scale %.2f,%.2f' % (self.x, self.y, self.z, self.scale_x, self.scale_y))
|
||||||
|
|
||||||
|
def set_scale(self, new_scale):
|
||||||
|
self.scale_x = self.scale_y = new_scale
|
||||||
|
|
||||||
|
def get_tile(self):
|
||||||
|
# adjust for brush size
|
||||||
|
size = self.app.ui.selected_tool.brush_size
|
||||||
|
if size:
|
||||||
|
size_offset = math.ceil(size / 2) - 1
|
||||||
|
return int(self.x + size_offset), int(-self.y + size_offset)
|
||||||
|
else:
|
||||||
|
return int(self.x), int(-self.y)
|
||||||
|
|
||||||
|
def center_in_art(self):
|
||||||
|
art = self.app.ui.active_art
|
||||||
|
if not art:
|
||||||
|
return
|
||||||
|
self.x = round(art.width / 2) * art.quad_width
|
||||||
|
self.y = round(-art.height / 2) * art.quad_height
|
||||||
|
self.moved = True
|
||||||
|
|
||||||
|
# !!TODO!! finish this, work in progress
|
||||||
|
def get_tiles_under_drag(self):
|
||||||
|
"""
|
||||||
|
returns list of tuple coordinates of all tiles under cursor's current
|
||||||
|
position AND tiles it's moved over since last update
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: get vector of last to current position, for each tile under
|
||||||
|
# current brush, do line trace along grid towards last point
|
||||||
|
|
||||||
|
# TODO: this works in two out of four diagonals,
|
||||||
|
# swap current and last positions to determine delta?
|
||||||
|
|
||||||
|
if self.last_x <= self.x:
|
||||||
|
x0, y0 = self.last_x, -self.last_y
|
||||||
|
x1, y1 = self.x, -self.y
|
||||||
|
else:
|
||||||
|
x0, y0 = self.x, -self.y
|
||||||
|
x1, y1 = self.last_x, -self.last_y
|
||||||
|
tiles = vector.get_tiles_along_line(x0, y0, x1, y1)
|
||||||
|
print('drag from %s,%s to %s,%s:' % (x0, y0, x1, y1))
|
||||||
|
print(tiles)
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
def get_tiles_under_brush(self):
|
||||||
|
"""
|
||||||
|
returns list of tuple coordinates of all tiles under the cursor @ its
|
||||||
|
current brush size
|
||||||
|
"""
|
||||||
|
size = self.app.ui.selected_tool.brush_size
|
||||||
|
tiles = []
|
||||||
|
x_start, y_start = int(self.x), int(-self.y)
|
||||||
|
for y in range(y_start, y_start + size):
|
||||||
|
for x in range(x_start, x_start + size):
|
||||||
|
tiles.append((x, y))
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
def undo_preview_edits(self):
|
||||||
|
for edit in self.preview_edits:
|
||||||
|
edit.undo()
|
||||||
|
|
||||||
|
def update_cursor_preview(self):
|
||||||
|
# rebuild list of cursor preview commands
|
||||||
|
if self.app.ui.selected_tool.show_preview:
|
||||||
|
self.preview_edits = self.app.ui.selected_tool.get_paint_commands()
|
||||||
|
for edit in self.preview_edits:
|
||||||
|
edit.apply()
|
||||||
|
else:
|
||||||
|
self.preview_edits = []
|
||||||
|
|
||||||
|
def start_paint(self):
|
||||||
|
if self.app.ui.console.visible or self.app.ui.popup in self.app.ui.hovered_elements:
|
||||||
|
return
|
||||||
|
if self.app.ui.selected_tool is self.app.ui.grab_tool:
|
||||||
|
self.app.ui.grab_tool.grab()
|
||||||
|
return
|
||||||
|
# start a new command group, commit and clear any preview edits
|
||||||
|
self.current_command = EditCommand(self.app.ui.active_art)
|
||||||
|
self.current_command.add_command_tiles(self.preview_edits)
|
||||||
|
self.preview_edits = []
|
||||||
|
self.app.ui.active_art.set_unsaved_changes(True)
|
||||||
|
#print(self.app.ui.active_art.command_stack)
|
||||||
|
|
||||||
|
def finish_paint(self):
|
||||||
|
"invoked by mouse button up and undo"
|
||||||
|
if self.app.ui.console.visible or self.app.ui.popup in self.app.ui.hovered_elements:
|
||||||
|
return
|
||||||
|
# push current command group onto undo stack
|
||||||
|
if not self.current_command:
|
||||||
|
return
|
||||||
|
self.current_command.finish_time = self.app.get_elapsed_time()
|
||||||
|
self.app.ui.active_art.command_stack.commit_commands([self.current_command])
|
||||||
|
self.current_command = None
|
||||||
|
# tools like rotate produce a different change each time, so update again
|
||||||
|
if self.app.ui.selected_tool.update_preview_after_paint:
|
||||||
|
self.update_cursor_preview()
|
||||||
|
#print(self.app.ui.active_art.command_stack)
|
||||||
|
|
||||||
|
def moved_this_frame(self):
|
||||||
|
return self.moved or \
|
||||||
|
int(self.last_x) != int(self.x) or \
|
||||||
|
int(self.last_y) != int(self.y)
|
||||||
|
|
||||||
|
def reposition_from_mouse(self):
|
||||||
|
self.x, self.y, _ = vector.screen_to_world(self.app,
|
||||||
|
self.app.mouse_x,
|
||||||
|
self.app.mouse_y)
|
||||||
|
|
||||||
|
def snap_to_tile(self):
|
||||||
|
w, h = self.app.ui.active_art.quad_width, self.app.ui.active_art.quad_height
|
||||||
|
char_aspect = w / h
|
||||||
|
# round result for oddly proportioned charsets
|
||||||
|
self.x = round(math.floor(self.x / w) * w)
|
||||||
|
self.y = round(math.ceil(self.y / h) * h * char_aspect)
|
||||||
|
|
||||||
|
def pre_first_update(self):
|
||||||
|
# vector.screen_to_world result will be off because camera hasn't
|
||||||
|
# moved yet, recalc view matrix
|
||||||
|
self.app.camera.calc_view_matrix()
|
||||||
|
self.reposition_from_mouse()
|
||||||
|
self.snap_to_tile()
|
||||||
|
self.update_cursor_preview()
|
||||||
|
self.entered_new_tile()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# save old positions before update
|
||||||
|
self.last_x, self.last_y = self.x, self.y
|
||||||
|
# pulse alpha and scale
|
||||||
|
self.alpha = 0.75 + (math.sin(self.app.get_elapsed_time() / 100) / 2)
|
||||||
|
#self.scale_x = 1.5 + (math.sin(self.get_elapsed_time() / 100) / 50 - 0.5)
|
||||||
|
mouse_moved = self.app.mouse_dx != 0 or self.app.mouse_dy != 0
|
||||||
|
# update cursor from mouse if: mouse moved, camera moved w/o keyboard
|
||||||
|
if mouse_moved or (not self.app.keyboard_editing and self.app.camera.moved_this_frame):
|
||||||
|
# don't let mouse move cursor if text tool input is happening
|
||||||
|
if not self.app.ui.text_tool.input_active:
|
||||||
|
self.reposition_from_mouse()
|
||||||
|
# cursor always at depth of active layer
|
||||||
|
art = self.app.ui.active_art
|
||||||
|
self.z = art.layers_z[art.active_layer] if art else 0
|
||||||
|
self.moved = True
|
||||||
|
if not self.moved and not self.app.ui.tool_settings_changed:
|
||||||
|
return
|
||||||
|
if not self.app.keyboard_editing and not self.app.ui.tool_settings_changed:
|
||||||
|
self.snap_to_tile()
|
||||||
|
# adjust for brush size
|
||||||
|
if self.app.ui.selected_tool.brush_size:
|
||||||
|
size = self.app.ui.selected_tool.brush_size
|
||||||
|
self.scale_x = self.scale_y = size
|
||||||
|
# don't reposition on resize if keyboard navigating
|
||||||
|
if mouse_moved:
|
||||||
|
size_offset = math.ceil(size / 2) - 1
|
||||||
|
self.x -= size_offset
|
||||||
|
self.y += size_offset
|
||||||
|
else:
|
||||||
|
self.scale_x = self.scale_y = 1
|
||||||
|
self.undo_preview_edits()
|
||||||
|
self.update_cursor_preview()
|
||||||
|
if self.moved_this_frame():
|
||||||
|
self.entered_new_tile()
|
||||||
|
|
||||||
|
def end_update(self):
|
||||||
|
"called at the end of App.update"
|
||||||
|
self.moved = False
|
||||||
|
|
||||||
|
def entered_new_tile(self):
|
||||||
|
if self.current_command and self.app.ui.selected_tool.paint_while_dragging:
|
||||||
|
# add new tile(s) to current command group
|
||||||
|
self.current_command.add_command_tiles(self.preview_edits)
|
||||||
|
self.app.ui.active_art.set_unsaved_changes(True)
|
||||||
|
self.preview_edits = []
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
GL.glUseProgram(self.shader.program)
|
||||||
|
GL.glUniformMatrix4fv(self.proj_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.projection_matrix)
|
||||||
|
GL.glUniformMatrix4fv(self.view_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.view_matrix)
|
||||||
|
GL.glUniform3f(self.position_uniform, self.x, self.y, self.z)
|
||||||
|
GL.glUniform3f(self.scale_uniform, self.scale_x, self.scale_y, self.scale_z)
|
||||||
|
GL.glUniform4fv(self.color_uniform, 1, self.color)
|
||||||
|
GL.glUniform2f(self.quad_size_uniform, self.app.ui.active_art.quad_width, self.app.ui.active_art.quad_height)
|
||||||
|
GL.glUniform1f(self.alpha_uniform, self.alpha)
|
||||||
|
# VAO vs non-VAO paths
|
||||||
|
if self.app.use_vao:
|
||||||
|
GL.glBindVertexArray(self.vao)
|
||||||
|
else:
|
||||||
|
attrib = self.shader.get_attrib_location # for brevity
|
||||||
|
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||||
|
GL.glVertexAttribPointer(attrib('vertPosition'), 2, GL.GL_FLOAT, GL.GL_FALSE, 0,
|
||||||
|
ctypes.c_void_p(0))
|
||||||
|
GL.glEnableVertexAttribArray(attrib('vertPosition'))
|
||||||
|
# bind elem array instead of passing it to glDrawElements - latter
|
||||||
|
# sends pyopengl a new array, which is deprecated and breaks on Mac.
|
||||||
|
# thanks Erin Congden!
|
||||||
|
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||||
|
GL.glEnable(GL.GL_BLEND)
|
||||||
|
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
|
||||||
|
# draw 4 corners
|
||||||
|
for i in range(4):
|
||||||
|
tx,ty = corner_transforms[i][0], corner_transforms[i][1]
|
||||||
|
ox,oy = corner_offsets[i][0], corner_offsets[i][1]
|
||||||
|
GL.glUniform2f(self.xform_uniform, tx, ty)
|
||||||
|
GL.glUniform2f(self.offset_uniform, ox, oy)
|
||||||
|
GL.glDrawElements(GL.GL_TRIANGLES, self.vert_count,
|
||||||
|
GL.GL_UNSIGNED_INT, None)
|
||||||
|
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||||
|
GL.glDisable(GL.GL_BLEND)
|
||||||
|
if self.app.use_vao:
|
||||||
|
GL.glBindVertexArray(0)
|
||||||
|
GL.glUseProgram(0)
|
||||||
|
# position and render tool icon
|
||||||
|
ui = self.app.ui
|
||||||
|
# special handling for quick grab
|
||||||
|
if self.app.right_mouse:
|
||||||
|
self.tool_sprite.texture = ui.grab_tool.get_icon_texture()
|
||||||
|
else:
|
||||||
|
self.tool_sprite.texture = ui.selected_tool.get_icon_texture()
|
||||||
|
# scale same regardless of screen resolution
|
||||||
|
aspect = self.app.window_height / self.app.window_width
|
||||||
|
scale_x = self.tool_sprite.texture.width / self.app.window_width
|
||||||
|
scale_x *= self.icon_scale_factor * self.app.ui.scale
|
||||||
|
self.tool_sprite.scale_x = scale_x
|
||||||
|
scale_y = self.tool_sprite.texture.height / self.app.window_height
|
||||||
|
scale_y *= self.icon_scale_factor * self.app.ui.scale
|
||||||
|
self.tool_sprite.scale_y = scale_y
|
||||||
|
# top left of icon at bottom right of cursor
|
||||||
|
size = ui.selected_tool.brush_size or 1
|
||||||
|
x, y = self.x, self.y
|
||||||
|
x += size * ui.active_art.quad_width
|
||||||
|
# non-square charsets a bit tricky to properly account for
|
||||||
|
char_aspect = ui.active_art.quad_height / ui.active_art.quad_width
|
||||||
|
y -= (size / char_aspect) * ui.active_art.quad_height
|
||||||
|
y *= char_aspect
|
||||||
|
sx, sy = vector.world_to_screen_normalized(self.app, x, y, self.z)
|
||||||
|
# screen-space offset by icon's height
|
||||||
|
sy -= scale_y
|
||||||
|
self.tool_sprite.x, self.tool_sprite.y = sx, sy
|
||||||
|
self.tool_sprite.render()
|
||||||
17
docs/bugs.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
bug list
|
||||||
|
|
||||||
|
PNG export: layers that have too high a Z won't show up in export, special-case behavior for near/far Z in renderable export?
|
||||||
|
|
||||||
|
rewrite Cursor.screen_to_world to produce same results as gluUnProject:
|
||||||
|
https://www.opengl.org/wiki/GluProject_and_gluUnProject_code
|
||||||
|
https://www.opengl.org/sdk/docs/man2/xhtml/gluUnProject.xml
|
||||||
|
|
||||||
|
more on above: when camera tilt engaged, cursor is closest to accurate along middle of bottom edge - apply aspect correction to both axes?
|
||||||
|
multiplying y by aspect (w/h) causes more distortion, but recenters most accurate point at middle of screen instead
|
||||||
|
|
||||||
|
|
||||||
|
lower priority:
|
||||||
|
|
||||||
|
problem discovered during 2015-01-04~06:
|
||||||
|
GLSL really can't handle int/uint attributes! charIndex looks fine in numpy int32 array data but comes into GLSL totally screwy. works fine when the array and attribute are floats instead. bug for PyOpenGL devs?
|
||||||
|
possible test program: two quads side by side, each doing some trivial shader that involves an arbitrary number, only difference being one is driven by an int attribute and the other by a float.
|
||||||
50
docs/bugs/jesper_sarnesjo.txt
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
~/src/_/playscii ♪ python3 playscii.py
|
||||||
|
Playscii v0.1.1
|
||||||
|
OpenGL detected: 4.1 NVIDIA-10.0.43 310.41.05f01
|
||||||
|
GLSL detected: 4.10
|
||||||
|
Vertex Array Object support found.
|
||||||
|
creating new document art/new.psci
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 41, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
TypeError: 'NoneType' object is not callable
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 531, in <module>
|
||||||
|
app = Application(log_file, log_lines, file_to_load)
|
||||||
|
File "playscii.py", line 117, in __init__
|
||||||
|
self.load_art(art_filename)
|
||||||
|
File "playscii.py", line 163, in load_art
|
||||||
|
art = self.new_art(filename)
|
||||||
|
File "playscii.py", line 139, in new_art
|
||||||
|
charset = self.load_charset(self.starting_charset)
|
||||||
|
File "playscii.py", line 190, in load_charset
|
||||||
|
new_charset = CharacterSet(self, charset_to_load, log)
|
||||||
|
File "/Users/jesper/src/_/playscii/charset.py", line 73, in __init__
|
||||||
|
self.texture = Texture(img.tostring(), self.image_width, self.image_height)
|
||||||
|
File "/Users/jesper/src/_/playscii/texture.py", line 16, in __init__
|
||||||
|
self.gltex = GL.glGenTextures(1)
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 61, in __call__
|
||||||
|
return self.wrapperFunction( self.baseFunction, *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/GL/exceptional.py", line 178, in glGenTextures
|
||||||
|
baseFunction( count, textures)
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 45, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/wrapper.py", line 664, in wrapperCall
|
||||||
|
raise err
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/wrapper.py", line 657, in wrapperCall
|
||||||
|
result = wrappedOperation( *cArguments )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/platform/baseplatform.py", line 402, in __call__
|
||||||
|
return self( *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/error.py", line 232, in glCheckError
|
||||||
|
baseOperation = baseOperation,
|
||||||
|
OpenGL.error.GLError: GLError(
|
||||||
|
err = 1282,
|
||||||
|
description = b'invalid operation',
|
||||||
|
baseOperation = glGenTextures,
|
||||||
|
pyArgs = (1, c_uint(1)),
|
||||||
|
cArgs = (1, <cparam 'P' (0x110ef9450)>),
|
||||||
|
cArguments = (1, <cparam 'P' (0x110ef9450)>)
|
||||||
|
)
|
||||||
61
docs/bugs/playscii_gl_errors.txt
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
winXP 32-bit virtualbox image, hardware acceleration enabled
|
||||||
|
|
||||||
|
|
||||||
|
OpenGL Warning: No pincher, please call crStateSetCurrentPointers() in your SPU
|
||||||
|
creating new document art/new.psci
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 491, in <module>
|
||||||
|
app = Application(log_file, log_lines, file_to_load)
|
||||||
|
File "playscii.py", line 87, in __init__
|
||||||
|
self.load_art(art_filename)
|
||||||
|
File "playscii.py", line 130, in load_art
|
||||||
|
art = self.new_art(filename)
|
||||||
|
File "playscii.py", line 106, in new_art
|
||||||
|
charset = self.load_charset(self.starting_charset)
|
||||||
|
File "playscii.py", line 159, in load_charset
|
||||||
|
new_charset = CharacterSet(self, charset_to_load, log)
|
||||||
|
File "c:\playscii\charset.py", line 73, in __init__
|
||||||
|
self.texture = Texture(img.tostring(), self.image_width, self.image_height)
|
||||||
|
File "c:\playscii\texture.py", line 24, in __init__
|
||||||
|
GL.glGenerateMipmap(GL.GL_TEXTURE_2D)
|
||||||
|
File "c:\python34\lib\site-packages\OpenGL\platform\baseplatform.py", line 407, in __call__
|
||||||
|
self.__name__, self.__name__,
|
||||||
|
OpenGL.error.NullFunctionError: Attempt to call an undefined function glGenerateMipmap, check for bool(glGenerateMipmap) before calling
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
bind VAO before texture stuff:
|
||||||
|
|
||||||
|
OpenGL Warning: No pincher, please call crStateSetCurrentPointers() in your SPU
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "c:\python34\lib\site-packages\OpenGL\latebind.py", line 41, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
TypeError: 'NoneType' object is not callable
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 491, in <module>
|
||||||
|
app = Application(log_file, log_lines, file_to_load)
|
||||||
|
File "playscii.py", line 87, in __init__
|
||||||
|
self.load_art(art_filename)
|
||||||
|
File "playscii.py", line 130, in load_art
|
||||||
|
art = self.new_art(filename)
|
||||||
|
File "playscii.py", line 106, in new_art
|
||||||
|
charset = self.load_charset(self.starting_charset)
|
||||||
|
File "playscii.py", line 159, in load_charset
|
||||||
|
new_charset = CharacterSet(self, charset_to_load, log)
|
||||||
|
File "c:\playscii\charset.py", line 73, in __init__
|
||||||
|
self.texture = Texture(img.tostring(), self.image_width, self.image_height)
|
||||||
|
File "c:\playscii\texture.py", line 17, in __init__
|
||||||
|
vao = GL.glGenVertexArrays(1)
|
||||||
|
File "c:\python34\lib\site-packages\OpenGL\latebind.py", line 45, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
File "c:\python34\lib\site-packages\OpenGL\wrapper.py", line 657, in wrapperCall
|
||||||
|
result = wrappedOperation( *cArguments )
|
||||||
|
File "c:\python34\lib\site-packages\OpenGL\platform\baseplatform.py", line 407, in __call__
|
||||||
|
self.__name__, self.__name__,
|
||||||
|
OpenGL.error.NullFunctionError: Attempt to call an undefined function glGenVertexArrays, check for bool(glGenVertexArrays) before calling
|
||||||
|
|
||||||
|
|
||||||
37
docs/bugs/terryc.txt
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File ".\OpenGL\latebind.py", line 41, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
TypeError: 'NoneType' object is not callable
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 489, in <module>
|
||||||
|
File "playscii.py", line 85, in __init__
|
||||||
|
File "playscii.py", line 128, in load_art
|
||||||
|
File "playscii.py", line 104, in new_art
|
||||||
|
File "playscii.py", line 157, in load_charset
|
||||||
|
File "c:\playscii\charset.py", line 73, in __init__
|
||||||
|
File "c:\playscii\texture.py", line 16, in __init__
|
||||||
|
File ".\OpenGL\latebind.py", line 61, in __call__
|
||||||
|
return self.wrapperFunction( self.baseFunction, *args, **named )
|
||||||
|
File ".\OpenGL\GL\exceptional.py", line 178, in glGenTextures
|
||||||
|
baseFunction( count, textures)
|
||||||
|
File ".\OpenGL\latebind.py", line 45, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
File ".\OpenGL\wrapper.py", line 664, in wrapperCall
|
||||||
|
raise err
|
||||||
|
File ".\OpenGL\wrapper.py", line 657, in wrapperCall
|
||||||
|
result = wrappedOperation( *cArguments )
|
||||||
|
File ".\OpenGL\platform\baseplatform.py", line 402, in __call__
|
||||||
|
return self( *args, **named )
|
||||||
|
File ".\OpenGL\error.py", line 232, in glCheckError
|
||||||
|
baseOperation = baseOperation,
|
||||||
|
OpenGL.error.GLError: GLError(
|
||||||
|
err = 1282,
|
||||||
|
description = b'invalid operation',
|
||||||
|
baseOperation = glGenTextures,
|
||||||
|
pyArgs = (1, c_ulong(0)),
|
||||||
|
cArgs = (1, <cparam 'P' (000000000464FB90)>),
|
||||||
|
cArguments = (1, <cparam 'P' (000000000464FB90)>)
|
||||||
|
)
|
||||||
5
docs/bugs/v21.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
tried launching in Parallels (as I run a Mac). Didn't work! playscii.log reads:
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 12, in <module>
|
||||||
|
File "c:\python34\lib\site-packages\zipextimporter.py", line 116, in load_module
|
||||||
|
zipimport.ZipImportError: can't find module sdl2
|
||||||
51
docs/bugs/zensaiyuki.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
Playscii v0.3.1
|
||||||
|
OpenGL detected: 4.1 INTEL-10.2.46
|
||||||
|
GLSL detected: 4.10
|
||||||
|
Vertex Array Object support found.
|
||||||
|
creating new document art/new.psci
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 41, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
TypeError: 'NoneType' object is not callable
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "playscii.py", line 664, in <module>
|
||||||
|
app = Application(log_file, log_lines, file_to_load)
|
||||||
|
File "playscii.py", line 123, in __init__
|
||||||
|
self.load_art(art_filename)
|
||||||
|
File "playscii.py", line 187, in load_art
|
||||||
|
art = self.new_art(filename)
|
||||||
|
File "playscii.py", line 163, in new_art
|
||||||
|
charset = self.load_charset(self.starting_charset)
|
||||||
|
File "playscii.py", line 223, in load_charset
|
||||||
|
new_charset = CharacterSet(self, charset_to_load, log)
|
||||||
|
File "/Users/bretonslivka/Downloads/JPLeBreton-playscii-337ccff3951d/charset.py", line 74, in __init__
|
||||||
|
self.texture = Texture(img.tostring(), self.image_width, self.image_height)
|
||||||
|
File "/Users/bretonslivka/Downloads/JPLeBreton-playscii-337ccff3951d/texture.py", line 16, in __init__
|
||||||
|
self.gltex = GL.glGenTextures(1)
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 61, in __call__
|
||||||
|
return self.wrapperFunction( self.baseFunction, *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/GL/exceptional.py", line 178, in glGenTextures
|
||||||
|
baseFunction( count, textures)
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/latebind.py", line 45, in __call__
|
||||||
|
return self._finalCall( *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/wrapper.py", line 664, in wrapperCall
|
||||||
|
raise err
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/wrapper.py", line 657, in wrapperCall
|
||||||
|
result = wrappedOperation( *cArguments )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/platform/baseplatform.py", line 402, in __call__
|
||||||
|
return self( *args, **named )
|
||||||
|
File "/usr/local/lib/python3.4/site-packages/OpenGL/error.py", line 232, in glCheckError
|
||||||
|
baseOperation = baseOperation,
|
||||||
|
OpenGL.error.GLError: GLError(
|
||||||
|
err = 1282,
|
||||||
|
description = b'invalid operation',
|
||||||
|
baseOperation = glGenTextures,
|
||||||
|
pyArgs = (1, c_uint(1)),
|
||||||
|
cArgs = (1, <cparam 'P' (0x101e1dbc0)>),
|
||||||
|
cArguments = (1, <cparam 'P' (0x101e1dbc0)>)
|
||||||
|
)
|
||||||
|
zenpsycho:JPLeBreton-playscii-337ccff3951d bretonslivka$
|
||||||
|
|
||||||
87
docs/design/early_notes.txt
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
|
||||||
|
second design approach circa first week of january 2015
|
||||||
|
|
||||||
|
art stores no intermediate lists, just arrays of ints for char, fg & bg colors
|
||||||
|
shader does the rest!
|
||||||
|
compute char UV in the vert shader
|
||||||
|
get fg and bg colors from index by sampling from a 1D palette texture
|
||||||
|
pass in texture dimensions as uniforms (2 bits of data that don't change per object)
|
||||||
|
art's get and set methods compute index into array and return/set directly
|
||||||
|
no layer or frame objects! an Art keeps lists of em, and a few lists for data that used to live in each instance eg frame delay and layer Z.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
design notes circa xmas 2014
|
||||||
|
|
||||||
|
art only changes when user edits
|
||||||
|
renderable updates from art when user edits or animation frame changes
|
||||||
|
|
||||||
|
is a texture lookup for the palette (1D or otherwise) even necessary? table of colors in the renderable's color buffers might be sufficient
|
||||||
|
|
||||||
|
is iterating through every tile in an art (layer) to update a renderable going to be bad for perf when lots of renderables are doing it every few frames?
|
||||||
|
PROBABLY, YES
|
||||||
|
|
||||||
|
so: arts generate the openGL arrays (verts, elements, UVs, colors) and keep them around, update them piecemeal and only when edits are made; renderables grab these arrays from their art and resubmit the buffer data as needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ascii engine - playscii?
|
||||||
|
early notes circa september/october 2014
|
||||||
|
|
||||||
|
core principles:
|
||||||
|
- ASCII art with transparency and multiple layers (mainly for easier editing but also FX)
|
||||||
|
- animation support - multiple frames per file, # of layers constant across frames
|
||||||
|
- edit (art and animation) mode integrated with live game mode, press a key to start editing a game you're playing
|
||||||
|
-- NOT a full game creation tool, no in-app code editing or visual scripting - game behavior defined through python objects/compionents
|
||||||
|
- important stuff hot reloads, definitely: sprites, animations, shaders(?), possibly: character sets, palettes
|
||||||
|
- MDI: edit multiple files
|
||||||
|
X developed in tandem + ships with example games in different styles: a top-down vs side view, realtime vs turn-based
|
||||||
|
-- on hold: all work goes towards Secret Game Project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- copy unity's rad ortho/persp camera mode switch a la https://twitter.com/pixelatedcrown/status/530857568240168960
|
||||||
|
- characters can be >1 gridsquares big (but are always 1:1 aspect?)
|
||||||
|
- objects can consist of >1 gridsquares
|
||||||
|
- files can have multiple "pages", eg for animation
|
||||||
|
-- how to define frame timing? for an anim, each page stays up for a certain time
|
||||||
|
- edit mode vs play mode
|
||||||
|
-- or: edit mode (like a level editor) vs paint mode (like edscii) vs play mode
|
||||||
|
- MDI: multiple files can be open, switch between em
|
||||||
|
- file references update when you change the referenced file (edit a sprite, see changes immediately)
|
||||||
|
- "transparent" is a valid BG or FG color
|
||||||
|
- objects can reference files, their pages define animations
|
||||||
|
- objects specify whether they move on char grid or pixel grid
|
||||||
|
- layers can have z-depths set, only drawn in "play" mode
|
||||||
|
- levels: single page of a file, collision can be painted in a special layer
|
||||||
|
- edit mode concepts: file, page/frame, layer, tile, character, color
|
||||||
|
- animation playback in sub window? edit while watching anim, set pages that define anim and their timings
|
||||||
|
- play mode concepts: world (collection of levels?), level, layer, object, sprite, animation
|
||||||
|
- levels (screens) can scroll on char grid or pixel grid
|
||||||
|
- selection of different CRT emulation shaders
|
||||||
|
|
||||||
|
test content:
|
||||||
|
- fireworks animation
|
||||||
|
- matrix screensaver-like noninteractive nonanimation
|
||||||
|
- endless ladder climbing remake
|
||||||
|
- real example game: "escape tunnel"
|
||||||
|
|
||||||
|
work:
|
||||||
|
- UI mockups
|
||||||
|
- architecture, UI/concept classes
|
||||||
|
- what drives object behavior?
|
||||||
|
- how does edit mode work exactly?
|
||||||
|
-- pick and place objects from a library?
|
||||||
|
-- how to edit object properties?
|
||||||
|
-- how to specify connections between objects?
|
||||||
|
|
||||||
|
references:
|
||||||
|
- Mark Wonnacott's kooltool: http://ragzouken.itch.io/kooltool
|
||||||
|
- libtcod / libdoryen: http://doryen.eptalys.net/libtcod
|
||||||
|
|
||||||
|
edscii daily doodle ideas:
|
||||||
|
- big wizard
|
||||||
|
- toucan
|
||||||
|
- tunnels under earth
|
||||||
BIN
docs/design/mock1.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/design/mock2.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
29
docs/design/old/20150101.txt
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
first design approach notes, started 2014-12-31 abandoned 2015-01-03
|
||||||
|
|
||||||
|
geo + uvs + color data for all layers on a frame are precomputed for each frame. when renderables animate they just bind different arrays
|
||||||
|
|
||||||
|
art.vert_array, art.elem_array: geo is same across all frames
|
||||||
|
ArtFrame.uv/fg/bg_array: uv/fg/bg color arrays for all layers
|
||||||
|
|
||||||
|
Art.init(): initialize lists for 1 frame with 1 blank layer of specified size, update all tiles' char/fg/bg
|
||||||
|
|
||||||
|
ArtFromDisk.init(): populate lists from saved data, update all tiles' char/fg/bg
|
||||||
|
|
||||||
|
art.build_geo(): create vert and elem arrays for given size and update all tiles' char/colors - okay if it's slower because this only happens on init/resize
|
||||||
|
|
||||||
|
ArtFrame.build_arrays(): creates uv/fgcolor/bgcolor arrays
|
||||||
|
|
||||||
|
art.set_char_index/color_at(): only called by user or sim edits, update internal lists only, add each changed tile to "to update" lists: "characters to update", "fg/bg colors to update"
|
||||||
|
|
||||||
|
art.update(): called from app.update(): process "to update" lists, calling art.update_char_array etc as needed (theoretrically this could be parallelized if lots of chars + colors are changing?)
|
||||||
|
|
||||||
|
art.update_char_array: computes index into uv array for given tile, sets uvs
|
||||||
|
|
||||||
|
art.update_color_array(fg=True): computers index into color array for given tile, sets color data
|
||||||
|
|
||||||
|
art.do_test(): add layers and frames, set chars and colors manually
|
||||||
|
|
||||||
|
renderable.update(): called from app.update(): has our art updated, or is it time to swap in a new animation frame? update buffers from art's arrays accordingly
|
||||||
|
|
||||||
|
- investigate whether storing color (vec4) per vertex is better than color index (U of 1D color texture?)
|
||||||
62
docs/design/old/art_class.txt
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
|
||||||
|
example: an art with 4 frames and 3 layers
|
||||||
|
|
||||||
|
Art
|
||||||
|
|width, height, charset, palette: stuff that's written to / read from disk
|
||||||
|
|renderables: list of renderables using us
|
||||||
|
|vert_array, elem_array: geo array for all layers (changes on: resize, layer add/del)
|
||||||
|
|update lists: tiles of specific frame+layers whose array data we should update
|
||||||
|
|frames
|
||||||
|
|0
|
||||||
|
|delay: time before display next frame
|
||||||
|
|uv_array, fg_color_array, bg_color array: arrays for all layers of this frame
|
||||||
|
| (changes on: tile edit, art resize, layer add/del)
|
||||||
|
|layers
|
||||||
|
|0
|
||||||
|
|z: z depth for this layer
|
||||||
|
|chars, fg_colors, bg_colors: data (lists of rows) for this layer
|
||||||
|
|1
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|2
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|1
|
||||||
|
|delay
|
||||||
|
|uv_array, fg_color_array, bg_color array
|
||||||
|
|layers
|
||||||
|
|0
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|1
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|2
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|2
|
||||||
|
|delay
|
||||||
|
|uv_array, fg_color_array, bg_color array
|
||||||
|
|layers
|
||||||
|
|0
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|1
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|2
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|3
|
||||||
|
|delay
|
||||||
|
|uv_array, fg_color_array, bg_color array
|
||||||
|
|layers
|
||||||
|
|0
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|1
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
|
|2
|
||||||
|
|z
|
||||||
|
|chars, fg_colors, bg_colors
|
||||||
BIN
docs/design/psmock3_full.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
docs/design/psmock3_statusbar.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
11
docs/docs_todo.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
documentation TODO
|
||||||
|
|
||||||
|
auto-generated docs, add strings to code as needed
|
||||||
|
|
||||||
|
readme in each example game dir explaining what it demonstrates
|
||||||
|
|
||||||
|
GameWorld/GameObject/GameHUD/GameRoom full public API
|
||||||
|
|
||||||
|
underlying Playscii classes, eg Art and Renderable
|
||||||
|
|
||||||
|
license clarification: any art or games you /create/ with Playscii does not inherit this license, you're free to set terms on your own work
|
||||||
21
docs/feedback.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
user feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
from @Flipyap 2015-01-24:
|
||||||
|
|
||||||
|
The way EDSCII works makes it pretty hard to shade things the old-fashioned ASCII way, without the use of colors.
|
||||||
|
|
||||||
|
I'd like to be able to bind characters to number keys for quick access to a palette of most frequently used characters/shapes.
|
||||||
|
|
||||||
|
The second one is weirder, but it's based on the way an ASCII artist's brain operates.
|
||||||
|
|
||||||
|
A pixel weight brush - draw random characters comprised of the same number of pixels (with a slider from fewest to most pixels).
|
||||||
|
|
||||||
|
With maybe a lock button to make the brush use the same set of characters for consistency.
|
||||||
|
|
||||||
|
Also, the current character palette makes it hard to distinguish connected shaped. It could use some spacing or a grid.
|
||||||
|
|
||||||
|
It would also be nice if different tool groups used different cursors. I often couldn't tell if I was in paint or erase mode.
|
||||||
|
|
||||||
|
---
|
||||||
BIN
docs/html/art_converters.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/html/art_crt_off.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/html/art_crt_on.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/html/art_layermenu.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/html/art_mask1.png
Normal file
|
After Width: | Height: | Size: 12 KiB |