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 |