Compare commits
462 Commits
Author | SHA1 | Date | |
---|---|---|---|
4c274bebb9 | |||
a28909bb70 | |||
6566f5140a | |||
|
776f8ed88c | ||
284a22c92e | |||
|
3d019d3ca8 | ||
99b749aaa2 | |||
|
d978d319bc | ||
|
87e896bd06 | ||
e233243b81 | |||
|
7ce3b8cd5d | ||
fa0a0a79ea | |||
|
d839356b2a | ||
|
7b019ce149 | ||
|
03c45a970c | ||
|
79d8ef3a44 | ||
|
c854d41579 | ||
6294f9c07f | |||
|
57605c969f | ||
e9374c5e6b | |||
b72e41d14d | |||
|
588357e5bf | ||
2031d7fa67 | |||
|
65ae99a26d | ||
|
5aaef15b2b | ||
|
60675d7859 | ||
|
b0ca1d4edf | ||
|
8f845d1e4c | ||
|
ca67d5d7f4 | ||
|
11daa8573c | ||
|
099a0eab31 | ||
|
6c0aaffd77 | ||
|
e744310861 | ||
|
96e9612d16 | ||
|
f05652d9b8 | ||
|
5e378fc2d0 | ||
|
519504fc32 | ||
|
0ea10546ac | ||
|
4c7a2e9f3b | ||
|
88471f4361 | ||
|
9df1ac7883 | ||
|
01cdea6edc | ||
|
b004dec8c9 | ||
|
13e3628668 | ||
|
12e19759aa | ||
|
d3607248c0 | ||
|
e639ad6255 | ||
|
236481ae1c | ||
|
18ace5144c | ||
|
6cf0590586 | ||
|
96bbc5088f | ||
|
26e66a5796 | ||
|
4f4223c514 | ||
|
893a23f95c | ||
|
a1d69203c9 | ||
b6a448b39b | |||
|
d2af345c0c | ||
|
67a9bda6e1 | ||
|
3758cb1336 | ||
44b967625c | |||
|
5694dd4dff | ||
|
2efa9d4575 | ||
|
16ddbd31f3 | ||
0821a5fa7a | |||
|
01ee49ddd4 | ||
|
cddff5c2d9 | ||
|
f4965039ee | ||
ce1d299de7 | |||
|
2b63f8b3f1 | ||
10a1b59b24 | |||
|
c9994423cb | ||
|
bc638b3bec | ||
|
258bd0d816 | ||
|
e96c50e81a | ||
|
82421be8bb | ||
|
1795d62294 | ||
|
0de2df0bd2 | ||
|
93e51d9240 | ||
|
568b8f3eba | ||
|
afaa12d86b | ||
|
3d48c43886 | ||
|
dfb591d410 | ||
|
841c7b9f90 | ||
|
e56fc502cb | ||
|
97ecd13c77 | ||
|
9516230893 | ||
|
5eb7699301 | ||
|
8d0d0883a3 | ||
5c07071cff | |||
|
0ef5875a72 | ||
|
92a3081b2e | ||
|
dd37c2f62f | ||
|
6d786c7358 | ||
|
d738029335 | ||
|
94c12541ef | ||
|
391fe6897b | ||
5b5180d44a | |||
|
1e5a268586 | ||
|
7bf1789239 | ||
|
8cb2b2388f | ||
|
93a9e5e4c4 | ||
|
ac4a73b2cb | ||
|
cbe3e226b4 | ||
|
eac9057f31 | ||
|
2eb42668c8 | ||
|
bdbf214d8d | ||
|
175706b1e4 | ||
|
9ff615a6b0 | ||
|
ad3cce116e | ||
|
949555ffff | ||
|
571857b063 | ||
|
d8d0bc6190 | ||
|
9e099d0715 | ||
|
746379bad6 | ||
|
591630b8a7 | ||
|
903a06c36c | ||
|
f6210a6356 | ||
|
da8cf238ee | ||
|
46a5dc6931 | ||
|
7e14122b8c | ||
|
df2c1a4b55 | ||
|
8e7029e34d | ||
|
1ab63434f6 | ||
|
afaa9d17cd | ||
|
9b853324ad | ||
|
c216a6089e | ||
|
a390f4f5e9 | ||
|
bde33e9232 | ||
|
0aa4eb9c0b | ||
4a80dc36ad | |||
|
7a4936e6a5 | ||
|
785ac403e3 | ||
|
f240cafa83 | ||
|
a497d08f31 | ||
|
5424c7cd98 | ||
|
dab84738d9 | ||
|
1270640619 | ||
|
8d7e264381 | ||
|
28a6532a21 | ||
|
fab1bee8d8 | ||
|
156e4a7e3a | ||
|
f48377e055 | ||
073e3d3740 | |||
|
120ec82d09 | ||
|
571e9db3e8 | ||
|
e56bdc16c2 | ||
|
5736c2300b | ||
2b3a8279b8 | |||
|
75e93611c3 | ||
|
ea58d5b426 | ||
|
b42f1277b1 | ||
|
196e3708d2 | ||
|
ac8c7a0a4c | ||
|
6673b67ffe | ||
|
c06f903a16 | ||
|
c6947fab44 | ||
|
641f5c7872 | ||
|
605696dddd | ||
|
5ba07afc9f | ||
|
e21d4d230c | ||
|
b0ac580677 | ||
|
d362bdc949 | ||
|
9c252a2bbc | ||
|
c959a9d865 | ||
|
8475e5228e | ||
|
20cbf546f9 | ||
|
49e261557c | ||
|
abbad0f352 | ||
|
05ccd0e339 | ||
|
c6f66d95f2 | ||
|
6fbc757f1e | ||
|
ffa7641b21 | ||
|
3229eb8ea7 | ||
|
42f0c195aa | ||
|
ddbd0299a0 | ||
|
5cbf15bef5 | ||
|
bb3422f7d8 | ||
fb47c15d6b | |||
|
affc1bae59 | ||
451dd9cba7 | |||
da47731faf | |||
|
7aeb659cf5 | ||
|
6c6a44fb18 | ||
|
478a655751 | ||
4acf6804d4 | |||
|
c36e68d6e4 | ||
|
e9c8f43e7e | ||
|
5579f5d791 | ||
|
9fb366aaab | ||
|
1a78ad584c | ||
|
a8c0c197ed | ||
00a4dec6a4 | |||
|
5ef12bef3d | ||
|
093c105120 | ||
|
b403bc4784 | ||
|
ae505166b7 | ||
d49c138257 | |||
|
0c2b10b031 | ||
|
887a190f11 | ||
|
41548504de | ||
|
a48e6325fe | ||
|
15ef6af998 | ||
|
4cd4bc9005 | ||
|
d06a405120 | ||
|
2dc178d67c | ||
|
77d501c389 | ||
|
10788a24ec | ||
|
4ad7d6c37c | ||
|
a9aeb9ca3a | ||
|
f3fe04e13a | ||
|
601062237d | ||
|
424044a5e4 | ||
|
3ace133037 | ||
|
4bddf076ea | ||
|
9b8dfb00da | ||
|
2f95ce3d5f | ||
|
9cb5c9157f | ||
|
bb77dab628 | ||
|
7f63ab2357 | ||
|
6341f39fb0 | ||
|
c378eead74 | ||
|
f821fef430 | ||
|
c329150aac | ||
|
73952a42f3 | ||
|
8e0b8d4fee | ||
|
7cd4721daa | ||
|
de3aba396d | ||
|
663fc0eecd | ||
|
6b7f8867fa | ||
|
9a56b4d7e9 | ||
|
8636d571b5 | ||
|
ad5ae22e5f | ||
|
505e0a4efb | ||
|
8afa082708 | ||
|
0b9606cbfe | ||
|
32072ade42 | ||
|
ed6457e94d | ||
|
4b174f26e4 | ||
|
411744bf10 | ||
|
ea5f5c1428 | ||
|
762bed888a | ||
|
f5e5e365d4 | ||
|
c01307202a | ||
|
b876dab156 | ||
|
ba3d979f9c | ||
|
9aa684fb77 | ||
|
9475725228 | ||
|
0394c5d15d | ||
|
e1918ab066 | ||
|
1b4612afd0 | ||
|
fe769c4488 | ||
|
dadafc84eb | ||
|
5fbb918132 | ||
|
86628fdea6 | ||
|
330d78702a | ||
|
77f52b6276 | ||
|
5ae49e71ff | ||
|
8ecbf13eae | ||
|
46ce7c33bf | ||
|
85870494a0 | ||
|
b8d32b29c8 | ||
|
c55a7451e7 | ||
|
9a556ba669 | ||
|
a5890a341d | ||
|
62ce2b5c71 | ||
|
a3e059a97b | ||
|
646e0063be | ||
|
3f62fbaa2b | ||
|
599f75b676 | ||
|
cd5b7086f2 | ||
|
7971a1f70e | ||
|
5eefe1937a | ||
|
288a6ac2c9 | ||
|
73e1fac89a | ||
|
158338637a | ||
|
8608ce346f | ||
|
1986630da1 | ||
|
04ae56e451 | ||
|
48318a91fe | ||
|
777668848e | ||
|
fb926f8c84 | ||
|
1cf5e7bd8b | ||
|
209bde5b5c | ||
|
895abe88ad | ||
|
3d7667573e | ||
|
fe9dfdf242 | ||
|
deb52d7350 | ||
|
e91deccb6f | ||
|
18ca083ba2 | ||
|
81de0d8eb0 | ||
|
a4a10e340d | ||
|
53d090a9c8 | ||
|
f453b82a58 | ||
|
7667079aa3 | ||
|
9d3e8a9822 | ||
|
d9912cacad | ||
|
45f420aaef | ||
|
e7f24c2371 | ||
|
1afa397fec | ||
|
d3c14a48ee | ||
|
089a247b2f | ||
|
d50f6701f4 | ||
|
99352bc1d5 | ||
|
7179346e2b | ||
|
c8b07b3bf5 | ||
|
757a460a44 | ||
|
6a4d13c726 | ||
|
3a8549cfcc | ||
|
98b5dd64a8 | ||
|
7fb743eb72 | ||
|
bbe37eab97 | ||
|
b9b776b7ad | ||
|
7ba49277a9 | ||
|
3c614dcca9 | ||
|
8751120fe1 | ||
|
7db2dd2b46 | ||
|
29798c135e | ||
|
45120d0c2b | ||
|
302017222d | ||
|
021731b740 | ||
|
d40a61554e | ||
|
53cb6a89ae | ||
|
01cc77e146 | ||
|
cc6033e8e4 | ||
|
6d4c0b2ca3 | ||
|
4dbd4f7912 | ||
|
2d5a2e4c87 | ||
|
56627f3e84 | ||
|
05e79c14e3 | ||
|
405ee895d8 | ||
|
19b82ff71a | ||
|
657f4e5209 | ||
|
cea015d015 | ||
|
39787861dc | ||
|
50d806cdcf | ||
|
7823a422b9 | ||
|
7ab4213273 | ||
|
e3ae7bcacf | ||
|
ca17bf424a | ||
|
2bb99a707e | ||
|
099508d4c0 | ||
|
57fab7db51 | ||
|
b24cc1877f | ||
|
8540a8f354 | ||
|
f648bcd7fb | ||
|
fdc2bcab8d | ||
|
7cfe55f42c | ||
|
2a1be4233b | ||
|
32e6eab943 | ||
|
d39427f978 | ||
|
866af98fe4 | ||
|
38842cee68 | ||
|
ba68e68584 | ||
|
6fa11d9dfe | ||
|
f8ca5b2cc5 | ||
|
7ad2cad77c | ||
|
f39113fd0b | ||
|
c38f8cdc53 | ||
|
f887a1f0aa | ||
|
ea672272f5 | ||
|
3985751bd1 | ||
|
ca2ae15117 | ||
|
4d60e95165 | ||
|
fb8b2aff01 | ||
|
bc40f8d0e6 | ||
|
013e81b3c0 | ||
|
25c42ea9e8 | ||
|
4b9399c687 | ||
|
3717429549 | ||
|
bc9c7cd7f7 | ||
|
a5c53c898e | ||
|
91a4514649 | ||
|
5b4dc601bc | ||
|
27fd73c96b | ||
|
056ca5cca8 | ||
|
c7545e53f7 | ||
|
0da7486750 | ||
|
a68b3a6d08 | ||
|
428bbae736 | ||
|
fbfcd5dae0 | ||
|
cdd527a7fd | ||
|
067570fd1a | ||
|
39af791012 | ||
|
3886bee1ba | ||
|
268e2d0dd2 | ||
|
da0d7e7055 | ||
|
654bab7c1d | ||
|
29142cd91c | ||
|
e5886bbe44 | ||
|
9f0a29302d | ||
|
6b09d488b6 | ||
|
aade89de7b | ||
|
f78c73a703 | ||
|
a34dae2ad0 | ||
|
5ce62c15f7 | ||
|
7c0cf3e029 | ||
|
8aad15f07b | ||
|
ffc8b90441 | ||
|
7d02604407 | ||
|
138b2c6d54 | ||
|
70ae60b9a4 | ||
|
8f85093eb8 | ||
|
d2d74c97a4 | ||
|
f07324662a | ||
|
31b7ece449 | ||
|
c151e0f656 | ||
|
4287b4f045 | ||
|
2498fd2a61 | ||
|
e3be4b4f3f | ||
|
cb18b3881f | ||
|
fbd9f0045b | ||
|
7bd74af1ad | ||
|
d85ee1758f | ||
|
ec4ac13231 | ||
|
b0e352444b | ||
|
5faebfe556 | ||
|
25ba94b9ac | ||
|
d987e60006 | ||
|
ca86572677 | ||
|
3f301423fb | ||
|
be9c726fa0 | ||
|
bad56ba442 | ||
|
fb8d8f033b | ||
|
b7f61d9485 | ||
|
5cdb12e8a8 | ||
|
4fdf08cab9 | ||
|
76bbee7e6d | ||
|
b35d666218 | ||
|
461d176ce4 | ||
|
f0488690b7 | ||
|
3282ebaefa | ||
|
0d3e33d960 | ||
|
1782ffcffb | ||
|
319d5b20eb | ||
|
0c25dd4ffe | ||
|
11e5ee5c24 | ||
|
2690eb8760 | ||
|
0726a8db9e | ||
|
1e48bd16b3 | ||
|
3e7dabc94e | ||
|
f2f34bfbc6 | ||
|
ca03caf3ba | ||
|
f2318ed308 | ||
|
8b187ec2b3 | ||
|
b6f5fe9364 | ||
|
23a2b4c0cf | ||
|
b3df257103 | ||
|
223c20e792 | ||
|
ca57fae19d | ||
|
9ca6561bc3 | ||
|
700ba16186 | ||
|
5fd599338f | ||
|
fb3f3ee5e8 | ||
|
ebb5f2e7d3 | ||
|
6b72f4b284 | ||
|
685606fdb6 | ||
|
7e63607836 | ||
|
62599ea72c | ||
|
84eebfd848 | ||
|
777f155d77 | ||
|
762fa9acd4 | ||
|
d697c50268 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
|
||||
env/
|
||||
venv/
|
||||
local/
|
||||
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
@ -23,3 +24,6 @@ save.json
|
||||
|
||||
# Don't commit docs output
|
||||
docs/_build
|
||||
|
||||
# Don't commit compiled messages
|
||||
*.mo
|
||||
|
@ -3,28 +3,58 @@ stages:
|
||||
- quality-assurance
|
||||
- build
|
||||
|
||||
py37:
|
||||
stage: test
|
||||
image: python:3.7-alpine
|
||||
before_script:
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
|
||||
py38:
|
||||
py310:
|
||||
stage: test
|
||||
image: python:3.8-alpine
|
||||
image: python:rc-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
only:
|
||||
- master
|
||||
|
||||
|
||||
py39:
|
||||
stage: test
|
||||
image: python:3.9-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
|
||||
|
||||
py38:
|
||||
stage: test
|
||||
image: python:3.8-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
|
||||
|
||||
py37:
|
||||
stage: test
|
||||
image: python:3.7-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
only:
|
||||
- master
|
||||
|
||||
|
||||
py36:
|
||||
stage: test
|
||||
image: python:3.6-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox
|
||||
script: tox -e py3
|
||||
only:
|
||||
- master
|
||||
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: python:3-alpine
|
||||
@ -33,11 +63,15 @@ linters:
|
||||
script: tox -e linters
|
||||
allow_failure: true
|
||||
|
||||
|
||||
build-deb:
|
||||
image: debian:buster-slim
|
||||
stage: build
|
||||
before_script:
|
||||
- apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper python3-all python3-setuptools
|
||||
- >
|
||||
apt-get update && apt-get -y --no-install-recommends install
|
||||
build-essential debmake dh-python debhelper gettext python3-all
|
||||
python3-setuptools
|
||||
script:
|
||||
- dpkg-buildpackage
|
||||
- mkdir build && cp ../*.deb build/
|
||||
@ -45,3 +79,5 @@ build-deb:
|
||||
paths:
|
||||
- build/*.deb
|
||||
expire_in: 1 week
|
||||
only:
|
||||
- master
|
||||
|
@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Squirrel Battle
|
||||
Copyright (C) 2020 ynerant
|
||||
Copyright (C) 2020-2021 ÿnérant, eichhornchen, nicomarg, charlse
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Squirrel Battle Copyright (C) 2020 ynerant
|
||||
Squirrel Battle Copyright (C) 2020-2021 ÿnérant, eichhornchen, nicomarg, charlse
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
68
README.md
68
README.md
@ -1,15 +1,77 @@
|
||||
[![pipeline status](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/pipeline.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
|
||||
[![coverage report](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/coverage.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
|
||||
[![Documentation Status](https://readthedocs.org/projects/squirrel-battle/badge/?version=latest)](https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest)
|
||||
[![PyPI](https://img.shields.io/pypi/v/dungeon-battle)](https://pypi.org/project/squirrel-battle/)
|
||||
[![Supported Python versions](https://img.shields.io/pypi/pyversions/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
|
||||
[![PyPI](https://img.shields.io/pypi/v/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
|
||||
[![PYPI downloads](https://img.shields.io/pypi/dm/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
|
||||
[![AUR version](https://img.shields.io/aur/version/python-squirrel-battle)](https://aur.archlinux.org/packages/python-squirrel-battle/)
|
||||
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Squirrel Battle
|
||||
|
||||
Attention aux couteaux des écureuils !
|
||||
Squirrel Battle is an infinite rogue-like game with randomly generated levels, in which the player controls a squirrel in its quest down in a dungeon, using diverse items to defeat monsters, and trying not to die.
|
||||
|
||||
## Installation
|
||||
|
||||
#### Via PyPI :
|
||||
```
|
||||
$ pip install --user squirrel-battle
|
||||
```
|
||||
to install
|
||||
|
||||
```
|
||||
$ pip install --user --upgrade squirrel-battle
|
||||
```
|
||||
to upgrade
|
||||
|
||||
#### Via ArchLinux package :
|
||||
Download one of these two packages on the AUR :
|
||||
|
||||
* python-squirrel-battle
|
||||
* python-squirrel-battle-git
|
||||
|
||||
#### Via Debian package :
|
||||
Available on our git repository, has a dependency on fonts-noto-color-emoji (to be found in the official Debian repositories).
|
||||
|
||||
Run
|
||||
```
|
||||
$ dpkg -i python3-squirrelbattle_23.14_all.deb
|
||||
```
|
||||
after downloading
|
||||
|
||||
In all cases, execute via command line : `squirrel-battle`
|
||||
|
||||
## For first-time players
|
||||
|
||||
The game is played in a terminal only, preferably one that supports color, markdown and emojis, but it can be played with only grey levels and relatively classic unicode characters.
|
||||
|
||||
Upon starting, the game will display the main menu. To navigate in menus, use zqsd or the keyboard arrows. To validate one of the options, use the Enter key. Mouse click is also supported in most menus, **but not in game**.
|
||||
|
||||
The game in itself can have two types of display : using ascii and simple unicode characters, or using emojis. To activate emoji mode, go to the settings menu and select the squirrel texture pack. Emojis will not work if the terminal does not support them, so do tests before to ensure the terminal can display them.
|
||||
|
||||
The game is translated (almost entirely) in English, French, German and Spanish. To change the language, go to the settings menu.
|
||||
|
||||
Controls in-game are pretty basic : use zqsd or the keyboard arrows to move. To hit an ennemy, simply go in its direction if it is in an adjacent tile.
|
||||
|
||||
There are several special control keys, they can be changed in the settings menu :
|
||||
|
||||
* To close a store menu or go back to the main menu, use Space
|
||||
* To open/close the inventory, use i
|
||||
* To use an object in the inventory, use u
|
||||
* To equip an object in the inventory, use e
|
||||
* To use a long range weapon after it has been equipped, use l and then select the direction to shoot in
|
||||
* To drop an object from the inventory, use r (to pick up an object, simply go on its tile, its automatic)
|
||||
* To talk to certains entities (or open a chest), use t and then select the direction of the entity
|
||||
* To wait a turn (rather than moving), use w
|
||||
* To dance and confuse the ennemies, use y
|
||||
* To use a ladder, use <
|
||||
|
||||
The dungeon consists in empty tiles (you can not go there), walls (which you can not cross) and floor ( :) ). Entities that move are usually monsters, but if you see a trumpet (or a '/'), do not kill it ! It is a familiar that will help you defeat monsters. Entities that do not move are either entities to which you can talk, like merchants and ... chests for some reason, or objects. Differentiating the two is not difficult, trying to go on the same tile as a living entity (or a chest) is impossible. Objects have pretty clear names, so it should not be too difficult determining what they do (if you still don't know, you can either read the docs, or test for yourself (beware of surprises though))
|
||||
|
||||
And that is all you need to get started! You can now start your adventure and don't worry, floors are randomly generated, so it won't always be the same boring level.
|
||||
|
||||
## Documentation
|
||||
|
||||
La documentation du projet est présente sur [squirrel-battle.readthedocs.io](https://squirrel-battle.readthedocs.io).
|
||||
The documentation for the project cen be found at [squirrel-battle.readthedocs.io](https://squirrel-battle.readthedocs.io). It is unfortunately only written in French.
|
||||
|
||||
Anyone interested in understanding how the code works can find a few explanations in the documentation.
|
||||
|
2
debian/README.debian
vendored
2
debian/README.debian
vendored
@ -2,4 +2,4 @@ Squirrel Battle
|
||||
|
||||
Watch out for squirrel's knifes!
|
||||
|
||||
-- Yohann D'ANELLO <ynerant@crans.org> Thu, 19 Nov 2020 03:30:42 +0100
|
||||
-- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 19 Nov 2020 03:30:42 +0100
|
||||
|
16
debian/changelog
vendored
16
debian/changelog
vendored
@ -1,5 +1,17 @@
|
||||
python3-squirrel-battle (3.14) beta; urgency=low
|
||||
python3-squirrel-battle (23.14) beta; urgency=low
|
||||
|
||||
* Big update
|
||||
|
||||
-- Yohann D'ANELLO <squirrel-battle@crans.org> Sun, 10 Jan 2021 23:56:42 +0100
|
||||
|
||||
python3-squirrel-battle (3.14.1) beta; urgency=low
|
||||
|
||||
* Some graphical improvements.
|
||||
|
||||
-- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 27 Nov 2020 18:25:42 +0100
|
||||
|
||||
python3-squirrel-battle (3.14) beta; urgency=low
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Yohann D'ANELLO <ynerant@crans.org> Thu, 19 Nov 2020 03:30:42 +0100
|
||||
-- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 19 Nov 2020 03:30:42 +0100
|
||||
|
4
debian/control
vendored
4
debian/control
vendored
@ -1,8 +1,8 @@
|
||||
Source: python3-squirrel-battle
|
||||
Section: devel
|
||||
Priority: optional
|
||||
Maintainer: ynerant <ynrant@crans.org>
|
||||
Build-Depends: debhelper (>=10~), dh-python, python3-all, python3-setuptools
|
||||
Maintainer: ynerant <squirrel-battle@crans.org>
|
||||
Build-Depends: debhelper (>=10~), dh-python, gettext, python3-all, python3-setuptools
|
||||
Depends: fonts-noto-color-emoji
|
||||
Standards-Version: 4.1.4
|
||||
Homepage: https://gitlab.crans.org/ynerant/squirrel-battle
|
||||
|
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -1,11 +1,11 @@
|
||||
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: Yohann D'ANELLO
|
||||
Upstream-Contact: Yohann D'ANELLO <ynerant@crans.org>
|
||||
Upstream-Name: ÿnérant, eichhornchen, nicomarg, charlse
|
||||
Upstream-Contact: ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
|
||||
Source: https://gitlab.crans.org/ynerant/squirrel-battle
|
||||
|
||||
Files: *
|
||||
Copyright: 2020 Yohann D'ANELLO <ynerant@crans.org>
|
||||
Copyright: 2020 ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
|
||||
License: GPL-3+
|
||||
This program is free software; you can redistribute it
|
||||
and/or modify it under the terms of the GNU General Public
|
||||
|
17
docs/conf.py
17
docs/conf.py
@ -18,8 +18,11 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Squirrel Battle'
|
||||
copyright = "2020"
|
||||
author = "Yohann D'ANELLO,\nMathilde DEPRES,\nNicolas MARGULIES,\nCharles PEYRAT"
|
||||
copyright = "2020-2021"
|
||||
author = "Yohann D'ANELLO,\n" \
|
||||
"Mathilde DEPRES,\n" \
|
||||
"Nicolas MARGULIES,\n" \
|
||||
"Charles PEYRAT"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
@ -57,4 +60,12 @@ html_theme = 'sphinx_rtd_theme'
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ['_static']
|
||||
|
||||
html_context = {
|
||||
'gitlab_user': 'ynerant',
|
||||
'gitlab_repo': 'squirrel-battle',
|
||||
'gitlab_host': 'gitlab.crans.org',
|
||||
'gitlab_version': 'master',
|
||||
'display_gitlab': True,
|
||||
}
|
||||
|
323
docs/deployment.rst
Normal file
323
docs/deployment.rst
Normal file
@ -0,0 +1,323 @@
|
||||
Déploiement du projet
|
||||
=====================
|
||||
|
||||
.. _PyPI: https://pypi.org/project/squirrel-battle/
|
||||
.. _AUR: https://aur.archlinux.org/packages/python-squirrel-battle/
|
||||
.. _Debian: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_23.14_all.deb?job=build-deb
|
||||
.. _installation: install.html
|
||||
|
||||
À chaque nouvelle version du projet, il est compilé et déployé dans PyPI_, dans
|
||||
l'AUR_ et un paquet Debian_ est créé, voir la page d'installation_.
|
||||
|
||||
|
||||
PyPI
|
||||
----
|
||||
|
||||
Définition du paquet
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _setup.py: https://gitlab.crans.org/ynerant/squirrel-battle/-/blob/master/setup.py
|
||||
|
||||
La documentation sur le packaging dans PyPI_ est disponible `ici
|
||||
<https://packaging.python.org/tutorials/packaging-projects/>`_.
|
||||
|
||||
Le fichier `setup.py`_ contient l'ensemble des instructions d'installation du
|
||||
paquet ainsi que des détails à fournir à PyPI :
|
||||
|
||||
.. code:: python
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
with open("README.md", "r") as f:
|
||||
long_description = f.read()
|
||||
|
||||
# Compile messages
|
||||
for language in ["de", "es", "fr"]:
|
||||
args = ["msgfmt", "--check-format",
|
||||
"-o", f"squirrelbattle/locale/{language}/LC_MESSAGES"
|
||||
"/squirrelbattle.mo",
|
||||
f"squirrelbattle/locale/{language}/LC_MESSAGES"
|
||||
"/squirrelbattle.po"]
|
||||
print(f"Compiling {language} messages...")
|
||||
subprocess.Popen(args)
|
||||
|
||||
setup(
|
||||
name="squirrel-battle",
|
||||
version="23.14",
|
||||
author="ÿnérant, eichhornchen, nicomarg, charlse",
|
||||
author_email="squirrel-battle@crans.org",
|
||||
description="Watch out for squirrel's knives!",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://gitlab.crans.org/ynerant/squirrel-battle",
|
||||
packages=find_packages(),
|
||||
license='GPLv3',
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console :: Curses",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Natural Language :: French",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Games/Entertainment",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
include_package_data=True,
|
||||
package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
Ce fichier contient le nom du paquet, sa version, l'auteur et son contact,
|
||||
sa description en une ligne et sa description longue, le lien d'accueil du projet,
|
||||
sa licence, ses classificateurs et son exécutable.
|
||||
|
||||
Il commence tout d'abord par compiler les fichiers de `traduction <translation.html>`_.
|
||||
|
||||
Le paramètre ``entry_points`` définit un exécutable nommé ``squirrel-battle``,
|
||||
qui permet de lancer le jeu.
|
||||
|
||||
|
||||
Installation locale du paquet
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
L'installation du paquet localement dans son environnement Python (virtuel ou non)
|
||||
peut se faire en exécutant ``pip install -e .``.
|
||||
|
||||
|
||||
Génération des binaires
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Les paquets ``setuptools`` (``python3-setuptools`` pour APT, ``python-setuptools``
|
||||
pour pacman) et ``wheel`` (``python3-wheel`` pour APT, ``python-wheel`` pour pacman)
|
||||
sont nécessaires. Une fois installés, il faut appeler la commande :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
||||
Une fois cela fait, le dossier ``dist/`` contiendra les archives à transmettre à PyPI.
|
||||
|
||||
|
||||
Publier sur PyPI
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Il faut avant tout avoir un compte sur PyPI. Dans `votre compte PyPI
|
||||
<https://pypi.org/manage/account/>`_, il faut générer un jeton d'accès API.
|
||||
|
||||
Dans le fichier ``.pypirc`` dans le répertoire principal de l'utilisateur,
|
||||
il faut ajouter le jeton d'accès :
|
||||
|
||||
.. code::
|
||||
|
||||
[pypi]
|
||||
username = __token__
|
||||
password = pypi-my-pypi-api-access-token
|
||||
|
||||
Cela permet de s'authentifier directement par ce jeton.
|
||||
|
||||
Ensuite, il faut installer ``twine``, qui permet de publier sur PyPI.
|
||||
|
||||
Il suffit ensuite d'appeler :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
twine upload dist/*
|
||||
|
||||
pour envoyer le paquet sur PyPI.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
À des fins de tests, il est possible d'utiliser le dépôt `<https://test.pypi.org>`_.
|
||||
Les différences sont au niveau de l'authentification, où il faut l'en-tête
|
||||
``[testpypi]`` dans le ``.pypirc``, et il faut envoyer le paquet avec
|
||||
``twine upload --repository testpypi dist/``.
|
||||
|
||||
|
||||
Publier dans l'AUR
|
||||
------------------
|
||||
|
||||
Fonctionnement du packaging
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _python-squirrel-battle: https://aur.archlinux.org/packages/python-squirrel-battle/
|
||||
.. _python-squirrel-battle-git: https://aur.archlinux.org/packages/python-squirrel-battle-git/
|
||||
|
||||
Deux paquets sont publiés dans l'AUR (Arch User Repository) :
|
||||
|
||||
- python-squirrel-battle_
|
||||
- python-squirrel-battle-git_
|
||||
|
||||
Le packaging dans Arch Linux se fait en commitant un fichier ``PKGBUILD`` dans
|
||||
le dépôt à l'adresse ``ssh://aur@aur.archlinux.org/packagename.git``,
|
||||
en remplaçant ``packagename`` par le nom du paquet.
|
||||
|
||||
Le second paquet compile directement le jeu à partir de la branche ``master``
|
||||
du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure :
|
||||
|
||||
.. code::
|
||||
|
||||
# Maintainer: Yohann D'ANELLO <squirrel-battle@crans.org>
|
||||
|
||||
pkgbase=squirrel-battle
|
||||
pkgname=python-squirrel-battle-git
|
||||
pkgver=23.14
|
||||
pkgrel=1
|
||||
pkgdesc="Watch out for squirrel's knives!"
|
||||
arch=('any')
|
||||
url="https://gitlab.crans.org/ynerant/squirrel-battle"
|
||||
license=('GPLv3')
|
||||
depends=('python')
|
||||
makedepends=('gettext' 'python-setuptools')
|
||||
depends=('noto-fonts-emoji')
|
||||
checkdepends=('python-tox')
|
||||
ssource=("git+https://gitlab.crans.org/ynerant/squirrel-battle.git")
|
||||
sha256sums=("SKIP")
|
||||
|
||||
pkgver() {
|
||||
cd pkgbase
|
||||
git describe --long --tags | sed -r 's/^v//;s/([^-]*-g)/r\1/;s/-/./g'
|
||||
}
|
||||
build() {
|
||||
cd $pkgbase
|
||||
python setup.py build
|
||||
}
|
||||
|
||||
check() {
|
||||
cd $pkgbase
|
||||
tox -e py3
|
||||
tox -e linters
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $pkgbase
|
||||
python setup.py install --skip-build \
|
||||
--optimize=1 \
|
||||
--root="${pkgdir}"
|
||||
install -vDm 644 README.md \
|
||||
-t "${pkgdir}/usr/share/doc/${pkgname}"
|
||||
install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
|
||||
}
|
||||
|
||||
Ces instructions permettent de cloner le dépôt, l'installer et exécuter des tests,
|
||||
en plus de définir les attributs du paquet.
|
||||
|
||||
Le fichier ``PKGBUILD`` du paquet ``python-squirrel-battle``, synchronisé avec
|
||||
les releases, est plus ou moins similaire :
|
||||
|
||||
.. code::
|
||||
|
||||
# Maintainer: Yohann D'ANELLO <squirrel-battle@crans.org>
|
||||
|
||||
pkgbase=squirrel-battle
|
||||
pkgname=python-squirrel-battle
|
||||
pkgver=23.14
|
||||
pkgrel=1
|
||||
pkgdesc="Watch out for squirrel's knives!"
|
||||
arch=('any')
|
||||
url="https://gitlab.crans.org/ynerant/squirrel-battle"
|
||||
license=('GPLv3')
|
||||
depends=('python')
|
||||
makedepends=('gettext' 'python-setuptools')
|
||||
depends=('noto-fonts-emoji')
|
||||
checkdepends=('python-tox')
|
||||
source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v23.14/$pkgbase-v$pkgver.tar.gz")
|
||||
sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0")
|
||||
|
||||
build() {
|
||||
cd $pkgbase-v$pkgver
|
||||
python setup.py build
|
||||
}
|
||||
|
||||
check() {
|
||||
cd $pkgbase-v$pkgver
|
||||
tox -e py3
|
||||
tox -e linters
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $pkgbase-v$pkgver
|
||||
python setup.py install --skip-build \
|
||||
--optimize=1 \
|
||||
--root="${pkgdir}"
|
||||
install -vDm 644 README.md \
|
||||
-t "${pkgdir}/usr/share/doc/${pkgname}"
|
||||
install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
|
||||
}
|
||||
|
||||
Il se contente ici de télécharger l'archive de la dernière release, et de travailler
|
||||
dessus.
|
||||
|
||||
|
||||
Mettre à jour
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Pour mettre à jour le dépôt, une fois les dépôts
|
||||
``ssh://aur@aur.archlinux.org/python-squirrel-battle.git`` et
|
||||
``ssh://aur@aur.archlinux.org/python-squirrel-battle-git.git`` clonés,
|
||||
il suffit de mettre à jour le paramètre ``pkgver`` pour la bonne version,
|
||||
de régénérer le fichier ``.SRCINFO`` en faisant
|
||||
``makepkg --printsrcinfo > .SRCINFO``, puis de committer/pousser.
|
||||
|
||||
|
||||
Construction du paquet Debian
|
||||
-----------------------------
|
||||
|
||||
Structure du paquet
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
L'ensemble des instructions pour construire le paquet Debian est situé dans le
|
||||
dossier ``debian/``.
|
||||
|
||||
Le fichier ``changelog`` est à modifier à chaque nouvelle version, le fichier
|
||||
``compat`` contient la version minimale de Debian requise (``10`` pour Debian
|
||||
Buster), le fichier ``copyright`` contient la liste des fichiers distribués sous
|
||||
quelle licence (ici GPLv3), le fichier ``control`` contient les informations du
|
||||
paquet, le fichier ``install`` les fichiers de configuration à installer
|
||||
(ici le fix de l'affichage de l'écurueil), et enfin le fichier ``rules`` l'ensemble
|
||||
des instructions à exécuter pour installer.
|
||||
|
||||
Le paquet ``fonts-noto-color-emoji`` est en dépendance pour le bon affichage
|
||||
des émojis.
|
||||
|
||||
Mettre à jour le paquet
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pour changer la version du paquet, il faut ajouter des lignes dans le fichier
|
||||
``changelog``.
|
||||
|
||||
|
||||
Construire le paquet
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Il faut partir d'une installation de Debian.
|
||||
|
||||
D'abord on installe les paquets nécessaires :
|
||||
|
||||
.. code::
|
||||
|
||||
apt update
|
||||
apt --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools
|
||||
|
||||
On peut ensuite construire le paquet :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
dpkg-buildpackage
|
||||
mkdir build && cp ../*.deb build/
|
||||
|
||||
Le paquet sera installé dans ``build/python3-squirrel-battle_23.14_all.deb``.
|
||||
|
||||
Le paquet Debian_ est construit par l'intégration continue Gitlab et ajouté
|
||||
à chaque release.
|
@ -3,13 +3,6 @@ Gestion de l'affichage
|
||||
|
||||
.. _curses: https://docs.python.org/3/howto/curses.html
|
||||
|
||||
L'intégralité de l'affichage du jeu est géré grâce à la bibliothèque native de
|
||||
Python curses_.
|
||||
|
||||
|
||||
.. warning::
|
||||
Plus de documentation à venir.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
@ -19,3 +12,51 @@ Python curses_.
|
||||
map
|
||||
stats
|
||||
logs
|
||||
|
||||
L'intégralité de l'affichage du jeu est géré grâce à la bibliothèque native de Python curses_.
|
||||
|
||||
Initialisation du terminal
|
||||
--------------------------
|
||||
|
||||
Au lancement du jeu, le terminal est initialisé, les caractères spéciaux sont traduits en abstraction curses, les
|
||||
caractères tapés par l'utilisateur ne sont plus affichés sur le terminal, on envoie les touches tapées à curses en
|
||||
permanence sans avoir à taper sur Entrée, le curseur est rendu invisible, on active le support des couleurs et enfin
|
||||
on déclare vouloir attraper tous les clics de souris. Tout cela est fait dans un contexte Python, qui permet
|
||||
d'effectuer les opérations inverses lors de la fermeture du programme, même en cas de crash, afin de retrouver un
|
||||
terminal utilisable.
|
||||
|
||||
|
||||
Pads
|
||||
----
|
||||
|
||||
Chaque morceau d'affichage est géré dans un *pad*. Un pad est un objet défini par curses, représentant une sous-fenêtre,
|
||||
qui a l'avantage d'être un peu plus flexible qu'une simple fenêtre. Un pad peut en effet dépasser de l'écran, et on
|
||||
peut choisir où placer le coin avant gauche et quelle partie du pad il faut dessiner sur la fenêtre.
|
||||
|
||||
Ce projet implémente une couche d'abstraction supplémentaire, afin d'avoir des objets plus utilisables. Chaque partie
|
||||
d'affichage est réprésentée dans une classé étendant ``Display``. Chaque ``Display`` contient un (ou plusieurs) pads,
|
||||
et propose une surcouche de certaines fonctions de curses.
|
||||
|
||||
L'affichage de texte par exemple sur un pad est plus sécurisée que celle proposée par curses. Le comportement par défaut
|
||||
est d'afficher un message à partir d'une position donnée avec des attributs (gras, couleur, ...) donnés sous
|
||||
forme numérique. Cette implémentation permet de passer les attributs voulus sous forme de paramètres booléens, de
|
||||
choisir la couleur de front/de fond sans définir de paire curses, mais surtout de tronquer le texte à la place
|
||||
disponible, afin de ne pas avoir à se soucier d'avoir un message trop grand et éviter des crashs non désirés.
|
||||
|
||||
Les ``Display`` sont gérés par un ``DisplayManager``. C'est lui qui décide, en fonction de l'état actuel du jeu,
|
||||
d'afficher les bons ``Display`` aux bons endroits et de les redimensionner correctement en fonction de la taille du
|
||||
terminal. C'est aussi lui qui propage l'information de modifier les attributs d'un ``Display``, si par exemple
|
||||
l'inventaire du joueur a été mis à jour.
|
||||
|
||||
Il s'occupe enfin de tout redimensionner si jamais le terminal a changé de taille, après une intervention
|
||||
de l'utilisateur.
|
||||
|
||||
|
||||
Interactions avec la souris
|
||||
---------------------------
|
||||
|
||||
Le jeu attrape les clics de souris. C'est le ``DisplayManager``, connaissant l'état du jeu et ce qui est affiché à
|
||||
quel endroit, qui va chercher sur quel ``Display`` on a cliqué. L'information est propagée au bon ``Display``, en
|
||||
adaptant les coordonnées.
|
||||
|
||||
Tout ``Display`` qui contient un menu procède de la même façon pour propager l'information au bon menu.
|
||||
|
@ -1,4 +1,7 @@
|
||||
Affichage de l'historique
|
||||
=========================
|
||||
|
||||
Pas encore documenté.
|
||||
L'historique des actions est affiché en bas de l'écran. À chaque action d'une entité, comme frapper quelqu'un,
|
||||
ou lorsque le joueur parle à une entité, cela s'affiche dans l'historique.
|
||||
|
||||
Il est affiché sur l'écran de jeu, en bas à gauche, occupant 1/5 de la hauteur et 4/5 de la largeur.
|
||||
|
@ -1,4 +1,13 @@
|
||||
Affichage de la carte
|
||||
=====================
|
||||
|
||||
Pas encore documenté.
|
||||
La carte s'affiche dans la partie en haut à gauche de l'écran, sur la plus grande partie de l'écran.
|
||||
On affiche les tuiles une par une. Selon le pack de textures utilisé, les tuiles prennent un ou deux caractères de large.
|
||||
Selon la visibilité de la case en fonction de la position du joueur, la couleur de la case sera plus ou moins sombre,
|
||||
voire masquée si le joueur n'a jamais vu la case. Les entités sont ensuite affichées, si elles sont dans le champ de
|
||||
vision du joueur.
|
||||
|
||||
La carte est actualisée lorsque cela est nécessaire, à chaque tick de jeu.
|
||||
|
||||
L'afffichage de la carte suit les déplacements du joueur, dans le sens où la caméra est toujours centrée sur lui.
|
||||
La carte prend 4/5 de l'affichage aussi bien en largeur qu'en hauteur.
|
||||
|
@ -1,4 +1,17 @@
|
||||
Affichage des menus
|
||||
===================
|
||||
|
||||
Pas encore documenté.
|
||||
Les menus sont affichés dans une boîte. On peut naviguer dedans avec les flèches haut et bas,
|
||||
et valider avec la touche entrée. Il est également possible d'intéragir avec la souris.
|
||||
|
||||
Il y a plusieurs menus dans le jeu :
|
||||
|
||||
* **Le menu principal**, qui s'affiche au lancement du jeu.
|
||||
* **Le menu des paramètres** : si on sélectionne un choix de touche et qu'on appuie sur entrée,
|
||||
on peut ensuite appuyer sur une touche pour remplacer la touche utilisée.
|
||||
* **Le menu des crédits** : ce menu fonctionne avec la souris. En cliquant on affiche une image.
|
||||
* **Le menu d'inventaire** : dans l'inventaire, on peut utiliser les touches pour utiliser un item ou l'équiper...
|
||||
* **Le menu de vente** : on peut utiliser les touches gauche et droite pour switcher entre l'inventaire du joueur
|
||||
et celui du marchand.
|
||||
* **Menu des warnings** : Pas vraiment un menu, mais affiche juste un message dans une petite boite pour prévenir
|
||||
le joueur que quelque chose ne va pas.
|
||||
|
@ -1,4 +1,31 @@
|
||||
Affichage des statistiques
|
||||
==========================
|
||||
|
||||
Pas encore documenté.
|
||||
.. _Hazel: ../index.html
|
||||
|
||||
Les statistiques du joueur sont affichées en haut à droite de l'écran
|
||||
et séparées du reste de l'affichage par une barre verticale, occupant 1/5 de la place horizontale.
|
||||
|
||||
Les informations affichées sont :
|
||||
|
||||
* **LVL** - le niveau du joueur
|
||||
* **EXP** - la quantité d'expérience que le joueur a gagné et combien il lui en faut avant le prochain niveau.
|
||||
* **HP** - la quantité de vie que le joueur a actuellement et combien il peut en avoir au maximum.
|
||||
* **STR** - la force du joueur.
|
||||
* **INT** - l'intelligence du joueur.
|
||||
* **CHR** - le charisme du joueur.
|
||||
* **DEX** - la dextérité du joueur.
|
||||
* **CON** - la constitution du joueur.
|
||||
* **CRI** - le pourcentage de chance de coup critique.
|
||||
* **Inventory** - le contenu de l'inventaire du joueur.
|
||||
* **Equipped main** - l'objet équipé dans la main principale.
|
||||
* **Equipped secondary** - l'objet équipé dans la main secondaire.
|
||||
* **Equipped armor** - le plastron porté par le joueur.
|
||||
* **Equipped helmet** - le casque porté par le joueur.
|
||||
* **Hazel** - le nombre d'Hazel_ que le joueur possède.
|
||||
* **Vous êtes mort** - Éventuellement, si le joueur est mort.
|
||||
|
||||
Si le joueur possède un `monocle <../entities/items.html#monocle>`_, alors les statistiques d'une entité proche sont
|
||||
également affichées dessous.
|
||||
|
||||
Des aides de jeu peuvent enfin être affichées en bas, telles que la touche sur laquelle il faut appuyer.
|
||||
|
31
docs/documentation.rst
Normal file
31
docs/documentation.rst
Normal file
@ -0,0 +1,31 @@
|
||||
Documentation
|
||||
=============
|
||||
|
||||
La documentation est gérée grâce à Sphinx. Le thème est le thème officiel de
|
||||
ReadTheDocs ``sphinx-rtd-theme``.
|
||||
|
||||
Générer localement la documentation
|
||||
-----------------------------------
|
||||
|
||||
On commence par se rendre au bon endroit et installer les bonnes dépendances :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
|
||||
La documentation se génère à partir d'appels à ``make``, selon le type de
|
||||
documentation voulue.
|
||||
|
||||
Par exemple, ``make html`` construit la documentation web, ``make latexpdf``
|
||||
construit un livre PDF avec cette documentation.
|
||||
|
||||
|
||||
Documentation externe
|
||||
---------------------
|
||||
|
||||
À chaque commit, un webhook est envoyé à `<readthedocs.io>`_, qui construit
|
||||
tout seul la documentation Sphinx, la publiant à l'adresse
|
||||
`<squirrel-battle.readthedocs.io>`_.
|
||||
|
||||
De plus, les documentations sont sauvegardées à chaque release taguée.
|
78
docs/entities/friendly.rst
Normal file
78
docs/entities/friendly.rst
Normal file
@ -0,0 +1,78 @@
|
||||
Entités pacifiques
|
||||
==================
|
||||
|
||||
.. _`entité attaquante`: index.html#entites-attaquantes
|
||||
.. _`pack de textures`: ../texture-pack.html
|
||||
|
||||
Chaque entité pacifique est en particulier une `entité attaquante`_,
|
||||
et hérite donc de ses attributs, et peut alors être attaquée.
|
||||
Ils sont cependant non-hostiles.
|
||||
|
||||
Il est possible d'interagir avec ces entités. En s'approchant d'elles, en
|
||||
appuyant sur la touche ``T`` suivie de la direction où regarder, un échange
|
||||
débute.
|
||||
|
||||
Si l'on s'adresse à un marchand, on devrait voir à l'écran l'inventaire du joueur
|
||||
et l'inventaire du marchand. Les flèches haut et bas permettent de sélectionner
|
||||
un objet, les touches droite et gauche de passer d'un inventaire à l'autre, et la
|
||||
touche entrée valide l'action.
|
||||
|
||||
On dénombre actuellement 3 types d'entités pacifiques :
|
||||
|
||||
Tournesol
|
||||
---------
|
||||
|
||||
Son nom est fixé à `sunflower`. Il a par défaut une **15** points de vie.
|
||||
|
||||
Interagir avec un tournesol n'a pas de réel intérêt, si ce n'est déclencher
|
||||
le « pouvoir des fleurs !! » ou bien savoir que « le soleil est chaud
|
||||
aujourd'hui ».
|
||||
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``I``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🌻``.
|
||||
|
||||
|
||||
Marchand
|
||||
--------
|
||||
|
||||
Son nom est fixé à `merchant`. Il a par défaut **5** points de vie.
|
||||
|
||||
En interagissant avec un marchand, il est possible de lui acheter et de lui
|
||||
vendre différents objets contre des Hazels, la monnaie du jeu.
|
||||
Les prix sont fixés :
|
||||
|
||||
* Anneau de coup critique : 15 Hazels
|
||||
* Anneau d'expérience : 25 Hazels
|
||||
* Arc : 22 Hazels
|
||||
* Baguette de feu : 36 Hazels
|
||||
* Bombe : 4 Hazels
|
||||
* Bouclier : 16 Hazels
|
||||
* Casque : 18 Hazels
|
||||
* Coeur : 3 Hazels
|
||||
* Épée : 20 Hazels
|
||||
* Monocle : 10 Hazels
|
||||
* Parchemin de dégâts : 18 Hazels
|
||||
* Parchemin de faiblesse : 13 Hazels
|
||||
* Plastron : 30 Hazels
|
||||
* Potion d'arrachage de corps : 14 Hazels
|
||||
* Règle : 2 Hazels
|
||||
|
||||
Le marchand commence avec 75 Hazels en sa possession, contre 42 pour le joueur.
|
||||
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``M``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦜``.
|
||||
|
||||
Trompette
|
||||
---------
|
||||
|
||||
Son nom est fixé à `trumpet`. Une trompette est un familier, c'est à dire que
|
||||
c'est une entité attaquante qui suit globalement le joueurs et attaque les monstres
|
||||
qui se rapprochent trop du joueur.
|
||||
|
||||
Elle a 20 points de vie et une attaque de 3.
|
||||
|
||||
Dans le `pack de textures`_ ASCII, elle est représentée par le caractère ``/``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, elle est représentée par l'émoji ``🎺``.
|
@ -7,6 +7,7 @@ Entités
|
||||
|
||||
player
|
||||
monsters
|
||||
friendly
|
||||
items
|
||||
|
||||
Entité
|
||||
@ -38,11 +39,12 @@ Entité attaquante
|
||||
-----------------
|
||||
|
||||
.. _monstre: monsters.html
|
||||
.. _entité pacifique: friendly.html
|
||||
.. _joueur: player.html
|
||||
|
||||
Une entité attaquante (``FightingEntity``) est un type d'entités représentant
|
||||
les personnages présents sur la carte, pouvant alors se battre. Ce peut être
|
||||
un monstre_ ou bien le joueur_.
|
||||
un monstre_, une `entité pacifique`_ ou bien le joueur_.
|
||||
|
||||
Elles disposent toutes, en plus des paramètres d'entité, des attributs suivants :
|
||||
|
||||
@ -77,3 +79,14 @@ en-dessous de 0 point de vie. À ce moment-là, l'entité est retirée de la car
|
||||
|
||||
Lorsqu'une entité en frappe une autre, celle-ci inflige autant de dégâts qu'elle
|
||||
n'a de force, et autant de points de vie sont perdus.
|
||||
|
||||
|
||||
Entité pacifique
|
||||
----------------
|
||||
|
||||
Une entité pacifique (``FriendlyEntity``) est un cas particulier d'entité
|
||||
attaquante. Contrairement aux montres, elles ne peuvent pas attaquer le joueur.
|
||||
|
||||
On peut parler à une entité pacifique en appuyant sur la touche ``T`` puis en
|
||||
appuyant sur la direction dans laquelle on veut parler à l'entité.
|
||||
|
||||
|
@ -19,7 +19,7 @@ Un objet dispose de deux paramètres :
|
||||
Si l'objet est dans l'inventaire, renvoie son propriétaire.
|
||||
|
||||
|
||||
Deux types d'objets sont pour l'instant présents :
|
||||
Il y a plusieurs types d'objets :
|
||||
|
||||
|
||||
Bombe
|
||||
@ -28,23 +28,143 @@ Bombe
|
||||
.. _entités attaquantes: index.html#entite-attaquante
|
||||
|
||||
Une bombe est un objet que l'on peut ramasser. Une fois ramassée, elle est placée
|
||||
dans l'inventaire. Le joueur peut ensuite lâcher la bombe, qui fera alors
|
||||
3 dégâts à toutes les `entités attaquantes`_ situées à moins de une case.
|
||||
dans l'inventaire. Le joueur peut ensuite utiliser la bombe, via l'inventaire
|
||||
ou après l'avoir équipée, qui fera alors 5 dégâts à toutes les
|
||||
`entités attaquantes`_ situées à moins de trois cases au bout de 4 ticks de jeu.
|
||||
|
||||
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ``o``
|
||||
et dans le `pack de textures`_ écureuil par l'émoji ``💣``.
|
||||
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ``ç``
|
||||
et dans le `pack de textures`_ écureuil par l'émoji ``💣``. Lors de l'explosion,
|
||||
la bombe est remplacée par un symbole ``%`` ou l'émoji ``💥`` selon le pack de
|
||||
textures utilisé.
|
||||
|
||||
.. note::
|
||||
|
||||
La gestion de l'inventaire n'ayant pas encore été implémentée, il n'est à
|
||||
l'heure actuelle pas possible de lancer une bombe.
|
||||
La bombe coûte 4 Hazels auprès des marchands.
|
||||
|
||||
|
||||
Cœur
|
||||
----
|
||||
|
||||
Une cœur est un objet que l'on ne peut pas ramasser. Dès que le joueur s'en
|
||||
approche, il est régénéré automatiquement de 3 points de vie, et le cœur disparaît.
|
||||
Un cœur est un objet que l'on ne peut pas ramasser. Dès que le joueur s'en
|
||||
approche ou qu'il l'achète auprès d'un marchand, il récupère automatiquement 5 points de vie, et le cœur disparaît.
|
||||
|
||||
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ``❤``
|
||||
Il est représenté dans le `pack de textures`_ ASCII par le caractère ``❤``
|
||||
et dans le `pack de textures`_ écureuil par l'émoji ``💜``.
|
||||
|
||||
Le cœur coûte 3 Hazels auprès des marchands.
|
||||
|
||||
|
||||
Potion d'arrachage de corps
|
||||
---------------------------
|
||||
|
||||
Cette potion permet, une fois utilisée, d'échanger toutes ses caractéristiques
|
||||
avec une autre entité aléatoire sur la carte. Cela inclut la force, la position,
|
||||
l'icône, ...
|
||||
|
||||
Elle est représentée par les caractères ``I`` et ``🔀``
|
||||
|
||||
Cette potion coûte 14 Hazels auprès des marchands.
|
||||
|
||||
|
||||
Règle
|
||||
-----
|
||||
|
||||
La règle est une arme que l'on peut trouver uniquement par achat auprès d'un
|
||||
marchand pour le coût de 2 Hazels ou dans un coffre. Une fois équipée, la règle ajoute 1 de force
|
||||
à son porteur.
|
||||
|
||||
Elle est représentée par les caractères ``\`` et ``📏``.
|
||||
|
||||
|
||||
Épée
|
||||
----
|
||||
|
||||
L'épée est une arme que l'on peut trouver uniquement par achat auprès d'un
|
||||
marchand pour le coût de 20 Hazels ou dans un coffre. Une fois équipée, l'épée ajoute 3 de force
|
||||
à son porteur.
|
||||
|
||||
Elle est représentée par les caractères ``†`` et ``🗡️``.
|
||||
|
||||
|
||||
Bouclier
|
||||
--------
|
||||
|
||||
Le bouclier est un type d'armure que l'on peut trouver uniquement par achat auprès d'un marchand pour le coût de 16 Hazels ou dans un coffre. Il s'équippe dans la main secondaire.
|
||||
Une fois équipé, le bouclier ajoute 2 de constitution à son porteur, lui permettant de parer mieux les coups.
|
||||
|
||||
Il est représenté par les caractères ``D`` et ``🛡️``.
|
||||
|
||||
Casque
|
||||
------
|
||||
|
||||
Le casque est un type d'armure que l'on peut trouver uniquement par achat auprès d'un marchand pour le coût de 18 Hazels ou dans un coffre. Il s'équippe sur la tête.
|
||||
Une fois équipé, le casque ajoute 2 de constitution à son porteur, lui permettant de prendre moins de dégâts.
|
||||
|
||||
Il est représenté par les caractères ``0`` et ``⛑️``.
|
||||
|
||||
Plastron
|
||||
--------
|
||||
|
||||
Le plastron est un type d'armure que l'on peut trouver uniquement par achat
|
||||
auprès d'un marchand pour le coût de 30 Hazels ou dans un coffre. Il s'équippe sur le corps.
|
||||
Une fois équipé, le casque ajoute 4 de constitution à son porteur,
|
||||
lui permettant de prendre moins de dégâts.
|
||||
|
||||
Il est représenté par les caractères ``(`` et ``🦺``.
|
||||
|
||||
Anneau
|
||||
------
|
||||
|
||||
Un anneau est un objet que l'on peut trouver uniquement par achat auprès d'un
|
||||
marchand ou dans un coffre. Il s'équippe sur la main secondaire.
|
||||
Une fois équipé, l'anneau ajoute un bonus à une ou plusieurs statistiques du
|
||||
joueur, améliorant sa capacité à se débarasser des monstres.
|
||||
|
||||
Il y a plusieurs types d'anneaux :
|
||||
|
||||
* **Anneau de coup critique**, qui augmente la chance de coup critique de 20%. Il coute 15 Hazels.
|
||||
* **Anneau de gain d'expérience amélioré**, qui multiplie le gain d'expérience du joueur par 2. Il coûte 25 Hazels.
|
||||
|
||||
Un anneau est représenté par les caractères ``o`` et ``💍``.
|
||||
|
||||
Monocle
|
||||
-------
|
||||
|
||||
L'anneau est un objet que l'on peut trouver uniquement par achat auprès d'un
|
||||
marchand pour le prix de 10 Hazels. On peut le trouver sinon dans les coffres.
|
||||
Il s'équippe sur la main secondaire.
|
||||
|
||||
Une fois porté, il permet de voir les caractéristiques des entités voisines
|
||||
(nom, force, chance de critique, ...).
|
||||
|
||||
Un monocle est représenté par les caractères ``ô`` et ``🧐``.
|
||||
|
||||
Parchemin
|
||||
---------
|
||||
|
||||
Un parchemin est un objet consommable qui se trouve chez un marchand ou dans un coffre. Lorsqu'il est utilisé, il a un effet sur les statistiques du joueur ou des autres entités combattantes. L'intensité de l'effet du parchemin dépend de l'intelligence du joueur.
|
||||
|
||||
Il y a plusieurs types de parchemins :
|
||||
|
||||
* **Parchemin de dégâts**, qui inflige des dégâts à toutes les entités combattantes qui sont à distance moins de 5 du joueur (ça touche aussi les familiers, mais pas le joueur). Le nombre de points de dégâts est directement l'intelligence du joueur. Il coute 18 Hazels.
|
||||
* **Parchemin de faiblesse**, qui réduit la force de toutes les entités sauf le joueur de min(1, intelligence//2) pendant 3 tours du jeu. Il coûte 13 Hazels.
|
||||
|
||||
Un parchemin est représenté par les caractères ``]`` et ``📜``.
|
||||
|
||||
Arc
|
||||
---
|
||||
|
||||
Un arc est une arme à distance qui s'équippe dans la main principale. Pour l'utiliser, il faut appuyer sur la touche de lancer (l de base) puis une touche de direction. Une flèche est alors tirée dans cette direction, et touche le premier ennemi qu'elle rencontre, s'il existe, sur les 3 premières cases dans cette direction.
|
||||
|
||||
La flèche fait 4 + dextérité du joueur dégâts.
|
||||
L'arc coûte 22 Hazels chez un marchand. On peut le trouver sinon dans les coffres.
|
||||
|
||||
Il est représenté par les caractères ``)`` et ``🏹``.
|
||||
|
||||
Baton de boule de feu
|
||||
---------------------
|
||||
|
||||
Un baton est une arme à distance qui s'équippe dans la main principale. Pour l'utiliser, il faut appuyer sur la touche de lancer (l de base) puis une touche de direction. Une boule de feu est alors tirée dans cette direction, et touche le premier ennemi qu'elle rencontre, s'il existe, sur les 4 premières cases dans cette direction. Lorsqu'un ennemi est touché, une explosion est affichée sur sa case.
|
||||
|
||||
La boule de feu fait 6 + intelligence du joueur dégâts.
|
||||
Le baton coûte 36 Hazels chez un marchand. On peut le trouver sinon dans les coffres.
|
||||
|
||||
Il est représenté par les caractères ``:`` et ``🪄``.
|
||||
|
@ -13,7 +13,7 @@ au plus vite sur le joueur pour le frapper selon l'algorithme de Dijkstra,
|
||||
et s'il est suffisamment proche frappe le joueur et lui fait autant de dégâts
|
||||
qu'il n'a de force.
|
||||
|
||||
On dénombre actuellement 4 types de monstres :
|
||||
On dénombre actuellement 5 types de monstres :
|
||||
|
||||
Hérisson
|
||||
--------
|
||||
@ -25,14 +25,14 @@ Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``*``.
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦔``.
|
||||
|
||||
|
||||
Castor
|
||||
------
|
||||
Tigre
|
||||
-----
|
||||
|
||||
Son nom est fixé à `beaver`. Il a par défaut une force à **2** et **20** points de vie.
|
||||
Son nom est fixé à `tiger`. Il a par défaut une force à **2** et **20** points de vie.
|
||||
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``_``.
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``n``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦫``.
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐅``.
|
||||
|
||||
|
||||
Lapin
|
||||
@ -40,6 +40,8 @@ Lapin
|
||||
|
||||
Son nom est fixé à `rabbit`. Il a par défaut une force à **1** et **15** points de vie.
|
||||
|
||||
Il a une chance de coup critique de 30%.
|
||||
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``Y``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐇``.
|
||||
@ -53,3 +55,14 @@ Son nom est fixé à `teddy_bear`. Il n'a pas de force et **50** points de vie.
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``8``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🧸``.
|
||||
|
||||
|
||||
Pygargue
|
||||
---------
|
||||
Son nom est fixé à `eagle`. Il a par défaut une force à **1000** et **5000** points de vie.
|
||||
|
||||
Il s'agit d'un boss difficilement tuable, qui apparait plus rarement que les autres monstres.
|
||||
|
||||
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``µ``.
|
||||
|
||||
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦅``.
|
||||
|
@ -5,6 +5,9 @@ Joueur
|
||||
.. _`paramètres`: ../settings.html
|
||||
.. _`pack de textures`: ../texture-pack.html
|
||||
.. _`objet`: items.html
|
||||
.. _`parchemins`: items.html#Parchemin
|
||||
.. _`batons` : items.html#Baton de boule de feu
|
||||
.. _`arc` : items.html#Arc
|
||||
|
||||
Le joueur est une `entité attaquante`_, contrôlée par l'utilisateur humain.
|
||||
|
||||
@ -32,6 +35,10 @@ Déplacement
|
||||
|
||||
Selon les paramètres_, il est possible de bouger le joueur dans les 4 directions
|
||||
en appuyant sur ``z``, ``q``, ``s``, ``d`` ou sur les flèches directionnelles.
|
||||
(ou sur d'autres touches selon ce qui est écrit dans le menu des paramètres)
|
||||
|
||||
Le joueur peut aussi ne rien faire pendant son tour, il suffit d'appuyer sur
|
||||
la touche d'attente (``w`` de base).
|
||||
|
||||
Le joueur se retrouvera bloqué s'il avance contre un mur. Si il avance sur un
|
||||
objet_, alors il prend l'objet_ et avance sur la case.
|
||||
@ -40,6 +47,25 @@ S'il rencontre une autre `entité attaquante`_, alors il frappe l'entité en
|
||||
infligeant autant de dégâts qu'il n'a de force. À chaque fois qu'une entité est
|
||||
tuée, le joueur gagne aléatoirement entre 3 et 7 points d'expérience.
|
||||
|
||||
Outre se déplacer et attaquer, le joueur peut utiliser la touche pour danser
|
||||
(``y`` de base) durant son tour et danser. Selon son charisme, il a plus ou moins
|
||||
de chances de rendre confus tous les ennemis à distance moins de 3. Un ennemi confus
|
||||
ne peut pas attaquer.
|
||||
|
||||
|
||||
Statistiques
|
||||
------------
|
||||
|
||||
Le joueur possède plusieurs statistiques :
|
||||
|
||||
* Niveau : son niveau, qui dépend de combien d'expérience il a accumulé
|
||||
* Expérience : la quantité d'expérience accumulée par le joueur, qui dépend de combien d'entités il a tué.
|
||||
* Force : indique combien de dommages le joueur inflige à ses ennemis
|
||||
* Intelligence : joue sur l'effet des objets magiques, tels que les `parchemins`_ ou les `batons`_
|
||||
* Charisme : joue sur l'efficacité de la danse du joueur
|
||||
* Dextérité : joue sur l'efficacité de l'`arc`_
|
||||
* Constitution : joue sur la quantité de dégâts que le joueur prend lorsqu'un monstre le frappe
|
||||
* Taux de critique : la chance (en pourcentage) que le joueur a de faire un coup critique
|
||||
|
||||
Expérience
|
||||
----------
|
||||
@ -49,4 +75,4 @@ Lorsque le joueur atteint la quantité d'expérience requise pour monter de nive
|
||||
le joueur gagne un niveau, regagne toute sa vie, consomme son expérience et la
|
||||
nouvelle quantité d'expérience requise est 10 fois le niveau actuel. De plus,
|
||||
entre 5 et 10 fois le niveau actuel entités apparaissent aléatoirement sur la
|
||||
carte à la montée de niveau. Enfin, le joueur gagne en force en montant de niveau.
|
||||
carte à la montée de niveau. Enfin, le joueur améliore ses statistiques en augmentant de niveau. Toutes les caractéristiques ne sont pas incrémentées à chaque niveau gagné.
|
||||
|
@ -13,11 +13,15 @@ Bienvenue dans la documentation de Squirrel Battle !
|
||||
:target: https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/dungeon-battle
|
||||
.. image:: https://img.shields.io/pypi/v/squirrel-battle
|
||||
:target: https://pypi.org/project/squirrel-battle/
|
||||
:alt: PyPI
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/dungeon-battle
|
||||
.. image:: https://img.shields.io/pypi/pyversions/squirrel-battle
|
||||
:target: https://pypi.org/project/squirrel-battle/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/squirrel-battle
|
||||
:target: https://pypi.org/project/squirrel-battle/
|
||||
:alt: PyPI downloads
|
||||
|
||||
@ -37,6 +41,9 @@ Bienvenue dans la documentation de Squirrel Battle !
|
||||
install-dev
|
||||
tests
|
||||
display/index
|
||||
translation
|
||||
deployment
|
||||
documentation
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
@ -1,16 +1,19 @@
|
||||
Installation d'un environnement de développement
|
||||
================================================
|
||||
|
||||
Il est toujours préférable de travailler dans un environnement Python isolé du reste de son instalation.
|
||||
Il est toujours préférable de travailler dans un environnement Python isolé du
|
||||
reste de son instalation.
|
||||
|
||||
1. **Installation des dépendances de la distribution.**
|
||||
Vous devez déjà installer Python et le module qui permet de créer des environnements virtuels.
|
||||
On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre.
|
||||
Vous devez déjà installer Python et le module qui permet de créer des
|
||||
environnements virtuels.
|
||||
On donne ci-dessous l'exemple pour une distribution basée sur Debian,
|
||||
mais vous pouvez facilement adapter pour ArchLinux ou autre.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ sudo apt update
|
||||
$ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev git
|
||||
$ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev gettext git
|
||||
|
||||
2. **Clonage du dépot** là où vous voulez :
|
||||
|
||||
@ -25,7 +28,13 @@ Il est toujours préférable de travailler dans un environnement Python isolé d
|
||||
|
||||
$ python3 -m venv env
|
||||
$ source env/bin/activate # entrer dans l'environnement
|
||||
(env)$ pip3 install -r requirements.txt
|
||||
(env)$ deactivate # sortir de l'environnement
|
||||
(env) $ pip3 install -r requirements.txt
|
||||
(env) $ deactivate # sortir de l'environnement
|
||||
|
||||
4. **Compilation des messages de traduction.**
|
||||
|
||||
.. code:: bash
|
||||
|
||||
(env) $ python3 main.py --compilemessages
|
||||
|
||||
Le lancement du jeu se fait en lançant la commande ``python3 main.py``.
|
@ -61,29 +61,19 @@ Le jeu peut être ensuite lancé via la commande ``squirrel-battle``.
|
||||
Sur Ubuntu/Debian
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14_all.deb?job=build-deb
|
||||
.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_23.14_all.deb?job=build-deb
|
||||
|
||||
Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit.
|
||||
Ils sont également attachés à chaque nouvelle release.
|
||||
|
||||
Il dépend du paquet ``fonts-noto-color-emoji``, permettant d'afficher les émojis
|
||||
dans le terminal. Il peut être installé via APT normalement sur une distribution
|
||||
récente, toutefois sur les versions les plus vieilles, incluant Debian Buster,
|
||||
certains émojis n'apparaissent pas. Il est essentiel de maintenir ce paquet à
|
||||
jour. Pour installer manuellement la dernière version de ce paquet,
|
||||
il suffit d'exécuter :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
rm fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
dans le terminal. Il peut être installé via APT.
|
||||
|
||||
Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
dpkg -i python3-squirrelbattle_3.14_all.deb
|
||||
dpkg -i python3-squirrelbattle_23.14_all.deb
|
||||
|
||||
Ce paquet inclut un patch pour afficher les émojis écureuil correctement.
|
||||
|
||||
|
14
docs/map.rst
14
docs/map.rst
@ -44,3 +44,17 @@ Mur
|
||||
Les murs délimitent les salles du donjon. Personne ne peut les traverser.
|
||||
Ils sont représentés par un dièse ``#`` dans le `pack de textures`_ ASCII et
|
||||
par une brique carrée ``🧱`` dans le `pack de textures`_ écureuil.
|
||||
|
||||
|
||||
Échelles
|
||||
~~~~~~~~
|
||||
|
||||
Les échelles sont les débuts et fin de niveau. Elles permettent de changer
|
||||
d'étage en appuyant sur une touche. Elles sont représentées par un ``H`` dans
|
||||
le `pack de textures`_ ASCII et par un émoji échelle ``🪜`` dans le
|
||||
`pack de textures`_ écureuil.
|
||||
|
||||
Lorsqu'on est sur l'échelle du début de niveau, appuyer sur ``<`` permet de
|
||||
monter d'un étage (revenir au niveau précédent). Lorsqu'on est sur l'échelle
|
||||
de fin de niveau, on génère une nouvelle carte si ce n'est pas déjà fait avec
|
||||
des monstres plus forts, et on place le joueur sur cette nouvelle carte.
|
||||
|
@ -1,2 +1,2 @@
|
||||
sphinx
|
||||
sphinx-rtd-theme
|
||||
sphinx>=3.3
|
||||
sphinx-rtd-theme>=0.5
|
||||
|
@ -11,8 +11,9 @@ prêt à tout pour s'en sortir. Sa vision de rongeur lui permet d'observer
|
||||
l'intégralité de la carte_, et à l'aide d'objets_, il va pouvoir affronter
|
||||
les monstres_ présents dans le donjon et gagner en expérience et en force.
|
||||
|
||||
Le jeu fonctionne par niveau. À chaque niveau ``n`` du joueur, entre ``3n`` et
|
||||
``7n`` entités apparaissent aléatoirement sur la carte.
|
||||
Le jeu fonctionne par étage. À chaque étage, différents monstres sont présents,
|
||||
et à l'aide d'objets, il pourra progresser dans le donjon et descendre de plus
|
||||
en plus bas.
|
||||
|
||||
En tuant des ennemis, ce qu'il parvient à faire en fonçant directement sur eux
|
||||
ayant mangé trop de noisettes (ou étant armé d'un couteau), l'écureuil va
|
||||
|
@ -1,4 +1,41 @@
|
||||
Paramètres
|
||||
==========
|
||||
|
||||
Pas encore documenté.
|
||||
.. _pack de textures: texture-pack.html
|
||||
|
||||
Il est possible de changer les touches utilisées dans le jeu dans le menu des paramètres.
|
||||
|
||||
On peut aussi changer le `pack de textures`_ utilisé.
|
||||
|
||||
Touches
|
||||
-------
|
||||
|
||||
Les touches utilisées de base sont :
|
||||
|
||||
* **Aller vers le haut** : z
|
||||
* **Aller vers le haut (secondaire)** : ↑
|
||||
* **Aller vers le bas** : s
|
||||
* **Aller vers le bas (secondaire)** : ↓
|
||||
* **Aller à droite** : d
|
||||
* **Aller à droite (secondaire)** : →
|
||||
* **Aller à gauche** : q
|
||||
* **Aller à gauche (secondaire)** : ←
|
||||
* **Valider le choix** : Entrée
|
||||
* **Inventaire** : i
|
||||
* **Utiliser un objet** : u
|
||||
* **Équiper un objet** : e
|
||||
* **Lacher un objet** : r
|
||||
* **Parler** : t
|
||||
* **Attendre** : w
|
||||
* **Utiliser une arme à distance** : l
|
||||
* **Dancer** : y
|
||||
* **Utiliser une échelle** : <
|
||||
|
||||
Autres
|
||||
------
|
||||
|
||||
.. _ascii: texture-pack.html#Pack ASCII
|
||||
.. _squirrel: texture-pack.html#Pack Écureuil
|
||||
|
||||
* **Texture pack utilisé** : parmi ascii_ et squirrel_
|
||||
* **Langue utilisée** : parmi anglais, français, espagnol, allemand
|
@ -1,12 +1,50 @@
|
||||
Exécution des tests
|
||||
===================
|
||||
|
||||
.. note::
|
||||
La documentation va être revue ici.
|
||||
|
||||
Les tests sont gérés par ``pytest`` dans le module ``squirrelbattle.tests``.
|
||||
|
||||
``tox`` est un outil permettant de configurer l'exécution des tests. Ainsi, après
|
||||
installation de tox dans votre environnement virtuel via ``pip install tox``,
|
||||
il vous suffit d'exécuter ``tox -e py3`` pour lancer les tests et ``tox -e linters``
|
||||
pour vérifier la syntaxe du code.
|
||||
|
||||
Tests unitaires
|
||||
---------------
|
||||
|
||||
Les tests sont gérés par ``pytest`` dans le module ``squirrelbattle.tests``.
|
||||
Le module ``pytest-cov`` permet de mesurer la couverture du code.
|
||||
Pour lancer les tests, il suffit de lancer ``tox -e py3`` ou de manière équivalente
|
||||
``pytest --cov=squirrelbattle/ --cov-report=term-missing squirrelbattle/``
|
||||
|
||||
L'intégration continue lance les tests pour les versions de Python de 3.6 à 3.10,
|
||||
sur une distribution Alpine Linux.
|
||||
|
||||
Chaque partie du code est testée unitairement, pour obtenir une couverture
|
||||
maximale et assurer un bon fonctionnement. En particulier, le jeu est lancé
|
||||
en commençant sur une carte déterministe (non générée aléatoirement) chargée
|
||||
depuis ``assets/example_map.txt``, sur laquelle sont placés des ennemis et objets
|
||||
avec lesquels le joueur doit interagir. On vérifie qu'à chaque touche appuyée,
|
||||
il se passe le bon comportement. Le comportement des différents menus est
|
||||
également testé.
|
||||
|
||||
L'environnement de test ne disposant pas a priori d'un terminal, le jeu est
|
||||
conçu pour fonctionner sans support graphique, avec un terminal fictif où les
|
||||
primitives de curses sont implémentées pour ne rien faire. On ne peut alors
|
||||
pas s'assurer du bon fonctionnement de curses.
|
||||
|
||||
De plus, une très fine partie du code est ignorée lors de la couverture, ce
|
||||
qui correspond à la phase d'initialisation du terminal et à la boucle infinie
|
||||
qui reçoit les touches de l'utilisateur, qu'il est alors impossible de tester
|
||||
unitairement.
|
||||
|
||||
|
||||
Analyseur syntaxique
|
||||
--------------------
|
||||
|
||||
``flake8`` est utilisé en guise d'analyseur syntaxique. Il vérifie si le code
|
||||
est bien formatté afin d'assurer une certaine lisibilité. En particulier,
|
||||
il vérifie l'indentation, si chaque variable est bien utilisée, s'il n'y a pas
|
||||
d'import inutile et s'ils sont dans l'ordre lexicographique, si chaque ligne
|
||||
fait au plus 80 caractères et si la signature de chaque fonction est bien
|
||||
spécifiée.
|
||||
|
||||
Pour lancer l'analyse, ``tox -e linters`` suffit. L'intégration continue
|
||||
effectue cette analyse à chaque commit.
|
||||
|
@ -9,18 +9,30 @@ Pack de textures
|
||||
|
||||
.. _Joueur: entities/player.html
|
||||
.. _Hérisson: entities/monsters.html#herisson
|
||||
.. _Lapin: entities/monsters.html#lapin
|
||||
.. _Tigre: entities/monsters.html#tigre
|
||||
.. _Nounours: entities/monsters.html#nounours
|
||||
.. _Tournesol: entities/friendly.html#tournesol
|
||||
.. _Marchand: entities/friendly.html#marchand
|
||||
.. _Cœur: entities/items.html#coeur
|
||||
.. _Bombe: entities/items.html#bombe
|
||||
.. _Lapin: entities/monsters.html#lapin
|
||||
.. _Castor: entities/monsters.html#castor
|
||||
.. _Nounours: entities/monsters.html#nounours
|
||||
.. _Explosion: entities/items.html#bombe
|
||||
.. _Potion d'arrachage de corps: entities/items.html
|
||||
.. _Épée: entities/items.html#epee
|
||||
.. _Bouclier: entities/items.html#bouclier
|
||||
.. _Hazel: ../index.html
|
||||
.. _Plastron: ../entities/items.html#plastron
|
||||
.. _Pygargue: ../entities/monsters.html#Pygargue
|
||||
.. _Casque: ../entities/items.html#Casque
|
||||
.. _Anneau: ../entities/items.html#Anneau
|
||||
.. _Trompette: ../entities/items.html#Trompette
|
||||
|
||||
Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour
|
||||
être affiché dans le terminal. Cependant, afin de pouvoir proposer plusieurs
|
||||
expériences graphiques (notamment en fonction du support des émojis), différents
|
||||
packs de textures sont proposés.
|
||||
|
||||
Il est possible de changer de pack dans les paramètres.
|
||||
Il est possible de changer de pack dans les paramètres_.
|
||||
|
||||
Les packs de textures peuvent influencer la taille que prennent les tuiles_,
|
||||
en raison du fait que les émojis ne sont pas monospace.
|
||||
@ -39,11 +51,23 @@ Chaque tuile fait un caractère de large.
|
||||
* Entités
|
||||
* Joueur_ : ``@``
|
||||
* Hérisson_ : ``*``
|
||||
* Lapin_ : ``Y``
|
||||
* Tigre_ : ``n``
|
||||
* Nounours_ : ``8``
|
||||
* Tournesol_ : ``I``
|
||||
* Marchand_ : ``M``
|
||||
* Cœur_ : ``❤``
|
||||
* Bombe_ : ``o``
|
||||
* Lapin_ : ``Y``
|
||||
* Castor_ : ``_``
|
||||
* Nounours_ : ``8``
|
||||
* Explosion_ : ``%``
|
||||
* `Potion d'arrachage de corps`_ : ``S``
|
||||
* Épée_ : ``†``
|
||||
* Bouclier_ : ``D``
|
||||
* Hazel_ : ``¤``
|
||||
* Plastron_ : ``(``
|
||||
* Pygargue_ : ``µ``
|
||||
* Casque_ : ``0``
|
||||
* Anneau_ : ``o``
|
||||
* Trompette_ : ``/``
|
||||
|
||||
|
||||
Pack Écureuil
|
||||
@ -58,8 +82,20 @@ Chaque tuile fait 2 caractères de large pour afficher les émojis proprement.
|
||||
* Entités
|
||||
* Joueur_ : ``🐿``
|
||||
* Hérisson_ : ``🦔``
|
||||
* Lapin_ : ``🐇``
|
||||
* Tigre_ : ``🐅``
|
||||
* Nounours_ : ``🧸``
|
||||
* Tournesol_ : ``🌻``
|
||||
* Marchand_ : ``🦜``
|
||||
* Cœur_ : ``💜``
|
||||
* Bombe_ : ``💣``
|
||||
* Lapin_ : ``🐇``
|
||||
* Castor_ : ``🦫``
|
||||
* Nounours_ : ``🧸``
|
||||
* Explosion_ : ``💥``
|
||||
* `Potion d'arrachage de corps`_ : ``🔀``
|
||||
* Épée_ : ``🗡️``
|
||||
* Bouclier_ : ``🛡️``
|
||||
* Hazel_ : ``🌰``
|
||||
* Plastron_ : ``🦺``
|
||||
* Pygargue_ : ``🦅``
|
||||
* Casque_ : ``⛑️``
|
||||
* Anneau_ : ``💍``
|
||||
* Trompette_ : ``🎺``
|
||||
|
120
docs/translation.rst
Normal file
120
docs/translation.rst
Normal file
@ -0,0 +1,120 @@
|
||||
Traduction
|
||||
==========
|
||||
|
||||
Le jeu Squirrel Battle est entièrement traduit en anglais, en français et en allement.
|
||||
La langue se choisit dans les `paramètres <settings.html>`_.
|
||||
|
||||
|
||||
Utitisation
|
||||
-----------
|
||||
|
||||
Les traductions sont gérées grâce au module natif ``gettext``. Le module
|
||||
``squirrelbattle.translations`` s'occupe d'installer les traductions, et de
|
||||
donner les chaînes traduites.
|
||||
|
||||
Pour choisir la langue, il faut appeler ``Translator.setlocale(language: str)``,
|
||||
où ``language`` correspond au code à 2 lettres de la langue.
|
||||
|
||||
Enfin, le module expose une fonction ``gettext(str) -> str`` qui permet de
|
||||
traduire les chaînes.
|
||||
|
||||
Il est courant et recommandé d'importer cette fonction sous l'alias ``_``,
|
||||
afin de limiter la verbositer et de permettre de rendre facilement une chaîne
|
||||
traduisible.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from squirrelbattle.translations import gettext as _, Translator
|
||||
|
||||
Translator.setlocale("fr")
|
||||
print(_("I am a translatable string"))
|
||||
print("I am not translatable")
|
||||
|
||||
Si les traductions sont bien faites (voir ci-dessous), cela donnera :
|
||||
|
||||
.. code::
|
||||
|
||||
Je suis une chaîne traduisible
|
||||
I am not translatable
|
||||
|
||||
À noter que si la chaîne n'est pas traduite, alors par défaut on renvoie la
|
||||
chaîne elle-même.
|
||||
|
||||
|
||||
Extraction des chaînes à traduire
|
||||
---------------------------------
|
||||
|
||||
L'appel à ``gettext`` ne fait pas que traduire les chaînes : il est possible
|
||||
également d'extraire toutes les chaînes à traduire.
|
||||
|
||||
Il est nécessaire d'installer le paquet Linux ``gettext`` pour cela.
|
||||
|
||||
L'utilitaire ``xgettext`` s'occupe de cette extraction. Il s'utilise de la façon
|
||||
suivante :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
xgettext --from-code utf-8 -o output_file.po source_1.py ... source_n.py
|
||||
|
||||
Afin de ne pas avoir à sélectionner manuellement chaque fichier, il est possible
|
||||
d'appeler directement ``python3 main.py --makemessages``. Cela a pour effet
|
||||
d'exécuter pour chaque langue ``<LANG>`` :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
find squirrelbattle -iname '*.py' | xargs xgettext --from-code utf-8
|
||||
--add-comments
|
||||
--package-name=squirrelbattle
|
||||
--package-version=23.14
|
||||
"--copyright-holder=ÿnérant, eichhornchen, nicomarg, charlse"
|
||||
--msgid-bugs-address=squirrel-battle@crans.org
|
||||
-o squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po
|
||||
|
||||
Les fichiers de traductions se trouvent alors dans
|
||||
``squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po``.
|
||||
|
||||
|
||||
Traduire les chaînes
|
||||
--------------------
|
||||
|
||||
Après extraction des chaînes, les chaînes à traduire se trouvent dans
|
||||
``squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po``, comme indiqué
|
||||
ci-dessus.
|
||||
|
||||
Ce fichier peut-être édité avec un utilitaire tel que ``poedit``, sur
|
||||
l'interface Web sur `<https://translate.ynerant.fr/squirrel-battle/squirrel-battle>`_,
|
||||
mais surtout manuellement avec un éditeur de texte.
|
||||
|
||||
Dans ce fichier, on obtient pour chaque chaîne à traduire un paragraphe de la
|
||||
forme :
|
||||
|
||||
.. code:: po
|
||||
|
||||
#: main.py:4
|
||||
msgid "I am a translatable string"
|
||||
msgstr "Je suis une chaîne traduisible"
|
||||
|
||||
Il sufift de remplir les champs ``msgstr``.
|
||||
|
||||
|
||||
Compilation des chaînes
|
||||
-----------------------
|
||||
|
||||
Pour gagner en efficacité, les chaînes sont compilées dans un fichier avec
|
||||
l'extension ``.mo``. Ce sont ces fichiers qui sont lus par le module de traduction.
|
||||
|
||||
Pour compiler les traductions, c'est l'utilitaire ``msgfmt`` fourni toujours par
|
||||
le paquet Linux ``gettext`` que nous utilisons. Il s'utilise assez simplement :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
msgfmt po_file.po -o mo_file.mo
|
||||
|
||||
À nouveau, il est possible de compiler automatiquement les messages en exécutant
|
||||
``python3 main.py --compilemessages``.
|
||||
|
||||
.. warning::
|
||||
|
||||
On ne partagera pas dans le dépôt Git les fichiers compilé. En développement,
|
||||
on compilera soi-même les messages, et en production, la construction des
|
||||
paquets se charge de compiler automatiquement les traductions.
|
@ -24,21 +24,12 @@ Sous Ubuntu/Debian
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet
|
||||
`fonts-noto-color-emoji`. Toutefois, le rythme de mise à jour de Debian étant
|
||||
lent, le paquet le plus récent ne contient pas tous les émojis. Sur Debian,
|
||||
il faudra donc installer le paquet le plus récent, ce qui fonctionne sans
|
||||
dépendance supplémentaire :
|
||||
`fonts-noto-color-emoji`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
rm fonts-noto-color-emoji_0~20200916-1_all.deb
|
||||
|
||||
Il reste le problème de l'écureuil. Sous Ubuntu et Debian, le caractère écureuil
|
||||
existe déjà, mais ne s'affiche pas proprement. On peut appliquer un patch qui
|
||||
permet d'afficher les émojis correctement dans son terminal. Pour cela, il
|
||||
suffit de faire :
|
||||
Toutefois, un problème reste avec l'écureuil. Sous Ubuntu et Debian, le
|
||||
caractère écureuil existe déjà, mais ne s'affiche pas proprement. On peut
|
||||
appliquer un patch qui permet d'afficher les émojis correctement dans son
|
||||
terminal. Pour cela, il suffit de faire :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
24
main.py
24
main.py
@ -1,5 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from squirrelbattle.bootstrap import Bootstrap
|
||||
from squirrelbattle.translations import Translator
|
||||
|
||||
if __name__ == "__main__":
|
||||
Bootstrap.run_game()
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("--makemessages", "-mm", action="store_true",
|
||||
help="Extract translatable strings")
|
||||
parser.add_argument("--compilemessages", "-cm", action="store_true",
|
||||
help="Compile translatable strings")
|
||||
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
if args.makemessages:
|
||||
Translator.makemessages()
|
||||
elif args.compilemessages:
|
||||
Translator.compilemessages()
|
||||
else:
|
||||
Bootstrap.run_game()
|
||||
|
26
setup.py
26
setup.py
@ -1,17 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import subprocess
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
with open("README.md", "r") as f:
|
||||
long_description = f.read()
|
||||
|
||||
# Compile messages
|
||||
for language in ["de", "es", "fr"]:
|
||||
args = ["msgfmt", "--check-format",
|
||||
"-o", f"squirrelbattle/locale/{language}/LC_MESSAGES"
|
||||
"/squirrelbattle.mo",
|
||||
f"squirrelbattle/locale/{language}/LC_MESSAGES"
|
||||
"/squirrelbattle.po"]
|
||||
print(f"Compiling {language} messages...")
|
||||
subprocess.Popen(args)
|
||||
|
||||
setup(
|
||||
name="squirrel-battle",
|
||||
version="3.14",
|
||||
author="ynerant",
|
||||
author_email="ynerant@crans.org",
|
||||
description="Watch out for squirrel's knifes!",
|
||||
version="23.14",
|
||||
author="ÿnérant, eichhornchen, nicomarg, charlse",
|
||||
author_email="squirrel-battle@crans.org",
|
||||
description="Watch out for squirrel's knives!",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://gitlab.crans.org/ynerant/squirrel-battle",
|
||||
@ -32,7 +46,7 @@ setup(
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
include_package_data=True,
|
||||
package_data={"squirrelbattle": ["assets/*"]},
|
||||
package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game",
|
||||
|
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
44
squirrelbattle/assets/ascii-art-ecureuil.txt
Normal file
44
squirrelbattle/assets/ascii-art-ecureuil.txt
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
⋀
|
||||
┃|┃
|
||||
┃|┃ ▓▓▒ ▓▓
|
||||
┃|┃ ▓▓ ▓▓▒
|
||||
┃|┃ ▓▓▓ ▓▓ ▓▓▓ ▒▒▒▒▒▒▒▒▒
|
||||
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
┃|┃ ▓▓▓▬█▓▓▓▓▓▓▬█▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
┃|┃ ▓▓▓▓░██░░▓▓░░██░▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
━━▓▓▓▓━━ ▓▓░░░░░░░░ ░░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▓▓▓ ▓░░░░░░░░░░░░░░░░░░░░▓▓▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
┃ ▓▓▓▓▓ ▓░░░░░░░░▄▄▄▄░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
┃ ▓▓▓▓▓ ▓▓░░░░░░░░░░░░░░░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▓ ▓▓▓▓░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▓▓▓▒▒░░░░░░░░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▒░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▒▒░░░░░░░░░░░░▓▓▓▓▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▒░░░░░░░░░░░░░░░▓▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒
|
||||
▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▒░░░░░░░░░░░░░▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▒▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
|
||||
▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒
|
||||
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒
|
||||
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒
|
||||
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒
|
||||
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓ ░
|
||||
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓ ░░
|
||||
▓▓▓▓▓▓▓▓▒▒░░░▒▒▒▒░░░░░░▓▓░▒▒▒▓▓▓▓▓▓▓▓▓▓░░░ ░
|
||||
▓▓▓▓▓▓▓▒░░░░░░░░░▒░░░░░░░░░░░░▒▒▒▓▓▓▓▓▓▓▓░░ ░░▒
|
||||
░ ░░░░░▒░░░░░░▒░░░▒░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░▒
|
||||
▒▒░░▓▓░░▒░░░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░░░░░▒░░░░░▒ ░░
|
||||
▒▒▒▒▒▓▒▒▓░░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░
|
||||
▒▒█▒█▒▒▒▓░░▒░░░░░░░░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░
|
||||
▒▒▒▒█▒▒▒▒░░░░▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒░░░
|
||||
▓█▒▒▒▒█▒█▒▒▒▒░░▒░░░░░▒░░░░▒░░░░░░░░░░░░░░░░░▒░░░░▒░░░░░░░▒░░░░░▒▒
|
||||
██▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░▒░░░░░░▒░░░░░▒░░░░░
|
||||
▒▒▒▒█▒▒▒▒▒▒▒░░░░░░░░░░▒░░░░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░
|
||||
▒▒█▒▒▒▒▒░▒░▒░░░░▓▓▓░░░░░░░▒░░░░▒░░░▒░░░░░░░▓▓░░░░░░░░░░░░ ░
|
||||
▒▒▒▒▒▒▒░▒░░░▓▓▓▓▓▓░░░░░░░▒░░░░░░░░▒░░░░▓▓▓▓▓▓░░░░░░░░ ░
|
||||
░▓▓▓▓▓▓░░░░░░▒░░░░░░░░▒░░░░░░▓▓▓▓▓░░░ ░ ░░
|
@ -1,18 +1,18 @@
|
||||
1 6
|
||||
####### #############
|
||||
#.....# #...........#
|
||||
#.H...# #...........#
|
||||
#.....# #####...........#
|
||||
#.....# #...............#
|
||||
#.....# #...&........H..#
|
||||
#.##### #.###...........#
|
||||
#.# #.# #...........#
|
||||
#.# #.# #############
|
||||
#.# #.#
|
||||
#.#### #.#
|
||||
#....# #.#
|
||||
####.###################.#
|
||||
####&###################&#
|
||||
#.....................# #################
|
||||
#.....................# #...............#
|
||||
#.....................#######...............#
|
||||
#...........................................#
|
||||
#.....................&.....&...............#
|
||||
#.....................#######...............#
|
||||
####################### #################
|
||||
|
@ -1,6 +1,6 @@
|
||||
1 17
|
||||
########### #########
|
||||
#.........# #.......#
|
||||
#....H....# #.......#
|
||||
#.........# ############.......#
|
||||
#.........###############..........#.......##############
|
||||
#.........#........................#....................#
|
||||
@ -13,7 +13,7 @@
|
||||
########.##########......# #.........# #.........#
|
||||
#...........##......# #.........# #.........#
|
||||
#...........##......# #.........# #.........#
|
||||
#...........##......# #.........# ################.######
|
||||
#...........##..H...# #.........# ################.######
|
||||
#...........##......# #.........# #.................############
|
||||
#...........##......# ########.########.......#.........#..........#
|
||||
#...........##......# #...............#.......#.........#..........#
|
||||
|
41
squirrelbattle/assets/example_map_3.txt
Normal file
41
squirrelbattle/assets/example_map_3.txt
Normal file
@ -0,0 +1,41 @@
|
||||
1 6
|
||||
################################################################################
|
||||
#..............................................................................#
|
||||
#..#...........................................................................#
|
||||
#...........#..................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
#..............................................................................#
|
||||
################################################################################
|
@ -1,6 +1,9 @@
|
||||
from squirrelbattle.game import Game
|
||||
from squirrelbattle.display.display_manager import DisplayManager
|
||||
from squirrelbattle.term_manager import TermManager
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .display.display_manager import DisplayManager
|
||||
from .game import Game
|
||||
from .term_manager import TermManager
|
||||
|
||||
|
||||
class Bootstrap:
|
||||
|
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
@ -1,7 +1,12 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
from typing import Any, Optional, Union
|
||||
import sys
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from squirrelbattle.game import Game
|
||||
from squirrelbattle.tests.screen import FakePad
|
||||
|
||||
|
||||
@ -12,37 +17,194 @@ class Display:
|
||||
height: int
|
||||
pad: Any
|
||||
|
||||
_color_pairs = {(curses.COLOR_WHITE, curses.COLOR_BLACK): 0}
|
||||
_colors_rgb = {}
|
||||
|
||||
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
|
||||
self.screen = screen
|
||||
self.pack = pack or TexturePack.get_pack("ascii")
|
||||
|
||||
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
|
||||
"""
|
||||
Overwrites the native curses function of the same name.
|
||||
"""
|
||||
return curses.newpad(height, width) if self.screen else FakePad()
|
||||
|
||||
def truncate(self, msg: str, height: int, width: int) -> str:
|
||||
"""
|
||||
Truncates a string into a string adapted to the width and height of
|
||||
the screen.
|
||||
"""
|
||||
height = max(0, height)
|
||||
width = max(0, width)
|
||||
lines = msg.split("\n")
|
||||
lines = lines[:height]
|
||||
lines = [line[:width] for line in lines]
|
||||
return "\n".join(lines)
|
||||
|
||||
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
|
||||
"""
|
||||
Translates a tuple (R, G, B) into a curses color index.
|
||||
If we already have a color index, then nothing is processed.
|
||||
If this is a tuple, we construct a new color index if non-existing
|
||||
and we return this index.
|
||||
The values of R, G and B must be between 0 and 1000, and not
|
||||
between 0 and 255.
|
||||
"""
|
||||
if isinstance(color, tuple):
|
||||
# The color is a tuple (R, G, B), that is potentially unknown.
|
||||
# We translate it into a curses color number.
|
||||
if color not in self._colors_rgb:
|
||||
# The color does not exist, we create it.
|
||||
color_nb = len(self._colors_rgb) + 8
|
||||
self.init_color(color_nb, color[0], color[1], color[2])
|
||||
self._colors_rgb[color] = color_nb
|
||||
color = self._colors_rgb[color]
|
||||
return color
|
||||
|
||||
def addstr(self, pad: Any, y: int, x: int, msg: str,
|
||||
fg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_WHITE,
|
||||
bg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_BLACK,
|
||||
*, altcharset: bool = False, blink: bool = False,
|
||||
bold: bool = False, dim: bool = False, invis: bool = False,
|
||||
italic: bool = False, normal: bool = False,
|
||||
protect: bool = False, reverse: bool = False,
|
||||
standout: bool = False, underline: bool = False,
|
||||
horizontal: bool = False, left: bool = False,
|
||||
low: bool = False, right: bool = False, top: bool = False,
|
||||
vertical: bool = False, chartext: bool = False) -> None:
|
||||
"""
|
||||
Displays a message onto the pad.
|
||||
If the message is too large, it is truncated vertically and horizontally
|
||||
The text can be bold, italic, blinking, ... if the right parameters are
|
||||
given. These parameters are translated into curses attributes.
|
||||
The foreground and background colors can be given as curses constants
|
||||
(curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to
|
||||
the color. R, G, B must be between 0 and 1000, and not 0 and 255.
|
||||
"""
|
||||
height, width = pad.getmaxyx()
|
||||
# Truncate message if it is too large
|
||||
msg = self.truncate(msg, height - y, width - x - 1)
|
||||
if msg.replace("\n", "") and x >= 0 and y >= 0:
|
||||
fg_color = self.translate_color(fg_color)
|
||||
bg_color = self.translate_color(bg_color)
|
||||
|
||||
# Get the pair number for the tuple (fg, bg)
|
||||
# If it does not exist, create it and give a new unique id.
|
||||
if (fg_color, bg_color) in self._color_pairs:
|
||||
pair_nb = self._color_pairs[(fg_color, bg_color)]
|
||||
else:
|
||||
pair_nb = len(self._color_pairs)
|
||||
self.init_pair(pair_nb, fg_color, bg_color)
|
||||
self._color_pairs[(fg_color, bg_color)] = pair_nb
|
||||
|
||||
# Compute curses attributes from the parameters
|
||||
attr = self.color_pair(pair_nb)
|
||||
attr |= curses.A_ALTCHARSET if altcharset else 0
|
||||
attr |= curses.A_BLINK if blink else 0
|
||||
attr |= curses.A_BOLD if bold else 0
|
||||
attr |= curses.A_DIM if dim else 0
|
||||
attr |= curses.A_INVIS if invis else 0
|
||||
# Italic is supported since Python 3.7
|
||||
italic &= sys.version_info >= (3, 7,)
|
||||
attr |= curses.A_ITALIC if italic else 0
|
||||
attr |= curses.A_NORMAL if normal else 0
|
||||
attr |= curses.A_PROTECT if protect else 0
|
||||
attr |= curses.A_REVERSE if reverse else 0
|
||||
attr |= curses.A_STANDOUT if standout else 0
|
||||
attr |= curses.A_UNDERLINE if underline else 0
|
||||
attr |= curses.A_HORIZONTAL if horizontal else 0
|
||||
attr |= curses.A_LEFT if left else 0
|
||||
attr |= curses.A_LOW if low else 0
|
||||
attr |= curses.A_RIGHT if right else 0
|
||||
attr |= curses.A_TOP if top else 0
|
||||
attr |= curses.A_VERTICAL if vertical else 0
|
||||
attr |= curses.A_CHARTEXT if chartext else 0
|
||||
|
||||
return pad.addstr(y, x, msg, attr)
|
||||
|
||||
def init_pair(self, number: int, foreground: int, background: int) -> None:
|
||||
foreground = foreground if self.screen and curses.can_change_color() \
|
||||
and foreground < curses.COLORS \
|
||||
else curses.COLOR_WHITE
|
||||
background = background if self.screen and curses.can_change_color() \
|
||||
and background < curses.COLORS \
|
||||
else curses.COLOR_WHITE
|
||||
return curses.init_pair(number, foreground, background) \
|
||||
if self.screen else None
|
||||
if self.screen and curses.can_change_color() \
|
||||
and number < curses.COLOR_PAIRS else None
|
||||
|
||||
def color_pair(self, number: int) -> int:
|
||||
return curses.color_pair(number) if self.screen else 0
|
||||
return curses.color_pair(number) if self.screen \
|
||||
and number < curses.COLOR_PAIRS else 0
|
||||
|
||||
def init_color(self, number: int, red: int, green: int, blue: int) -> None:
|
||||
return curses.init_color(number, red, green, blue) \
|
||||
if self.screen and curses.can_change_color() \
|
||||
and number < curses.COLORS else None
|
||||
|
||||
def resize(self, y: int, x: int, height: int, width: int,
|
||||
resize_pad: bool = True) -> None:
|
||||
"""
|
||||
Resizes a pad.
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
if hasattr(self, "pad") and resize_pad:
|
||||
self.pad.resize(self.height, self.width)
|
||||
if hasattr(self, "pad") and resize_pad and \
|
||||
self.height >= 0 and self.width >= 0:
|
||||
self.pad.erase()
|
||||
self.pad.resize(self.height + 1, self.width + 1)
|
||||
|
||||
def refresh(self, *args, resize_pad: bool = True) -> None:
|
||||
"""
|
||||
Refreshes a pad
|
||||
"""
|
||||
if len(args) == 4:
|
||||
self.resize(*args, resize_pad)
|
||||
self.display()
|
||||
|
||||
def refresh_pad(self, pad: Any, top_y: int, top_x: int,
|
||||
window_y: int, window_x: int,
|
||||
last_y: int, last_x: int) -> None:
|
||||
"""
|
||||
Refreshes a pad on a part of the window.
|
||||
The refresh starts at coordinates (top_y, top_x) from the pad,
|
||||
and is drawn from (window_y, window_x) to (last_y, last_x).
|
||||
If coordinates are invalid (negative indexes/length...), then nothing
|
||||
is drawn and no error is raised.
|
||||
"""
|
||||
top_y, top_x = max(0, top_y), max(0, top_x)
|
||||
window_y, window_x = max(0, window_y), max(0, window_x)
|
||||
screen_max_y, screen_max_x = self.screen.getmaxyx() if self.screen \
|
||||
else (42, 42)
|
||||
last_y, last_x = min(screen_max_y - 1, last_y), \
|
||||
min(screen_max_x - 1, last_x)
|
||||
|
||||
if last_y >= window_y and last_x >= window_x:
|
||||
# Refresh the pad only if coordinates are valid
|
||||
pad.noutrefresh(top_y, top_x, window_y, window_x, last_y, last_x)
|
||||
|
||||
def display(self) -> None:
|
||||
"""
|
||||
Draw the content of the display and refresh pads.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
"""
|
||||
The game state was updated.
|
||||
Indicate what to do with the new state.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
"""
|
||||
A mouse click was performed on the coordinates (y, x) of the pad.
|
||||
Maybe it should do something.
|
||||
"""
|
||||
|
||||
@property
|
||||
def rows(self) -> int:
|
||||
return curses.LINES if self.screen else 42
|
||||
@ -50,3 +212,107 @@ class Display:
|
||||
@property
|
||||
def cols(self) -> int:
|
||||
return curses.COLS if self.screen else 42
|
||||
|
||||
|
||||
class VerticalSplit(Display):
|
||||
"""
|
||||
A class to split the screen in two vertically with a pretty line.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(self.rows, 1)
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 1
|
||||
|
||||
@width.setter
|
||||
def width(self, val: Any) -> None:
|
||||
pass
|
||||
|
||||
def display(self) -> None:
|
||||
for i in range(self.height):
|
||||
self.addstr(self.pad, i, 0, "┃")
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.y + self.height - 1, self.x)
|
||||
|
||||
|
||||
class HorizontalSplit(Display):
|
||||
"""
|
||||
A class to split the screen in two horizontally with a pretty line.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(1, self.cols)
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return 1
|
||||
|
||||
@height.setter
|
||||
def height(self, val: Any) -> None:
|
||||
pass
|
||||
|
||||
def display(self) -> None:
|
||||
for i in range(self.width):
|
||||
self.addstr(self.pad, 0, i, "━")
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y,
|
||||
self.x + self.width - 1)
|
||||
|
||||
|
||||
class Box(Display):
|
||||
"""
|
||||
A class for pretty boxes to print menus and other content.
|
||||
"""
|
||||
title: str = ""
|
||||
|
||||
def update_title(self, title: str) -> None:
|
||||
self.title = title
|
||||
|
||||
def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
self.fg_border_color = fg_border_color or curses.COLOR_WHITE
|
||||
|
||||
def display(self) -> None:
|
||||
self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓",
|
||||
self.fg_border_color)
|
||||
for i in range(1, self.height - 1):
|
||||
self.addstr(self.pad, i, 0, "┃", self.fg_border_color)
|
||||
self.addstr(self.pad, i, self.width - 1, "┃", self.fg_border_color)
|
||||
self.addstr(self.pad, self.height - 1, 0,
|
||||
"┗" + "━" * (self.width - 2) + "┛", self.fg_border_color)
|
||||
|
||||
if self.title:
|
||||
self.addstr(self.pad, 0, (self.width - len(self.title) - 8) // 2,
|
||||
f" == {self.title} == ", curses.COLOR_GREEN,
|
||||
italic=True, bold=True)
|
||||
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.y + self.height - 1, self.x + self.width - 1)
|
||||
|
||||
|
||||
class MessageDisplay(Display):
|
||||
"""
|
||||
A class to handle the display of popup messages.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs)
|
||||
self.message = ""
|
||||
self.pad = self.newpad(1, 1)
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.message = game.message
|
||||
|
||||
def display(self) -> None:
|
||||
self.box.refresh(self.y - 1, self.x - 2,
|
||||
self.height + 2, self.width + 4)
|
||||
self.box.display()
|
||||
self.pad.erase()
|
||||
self.addstr(self.pad, 0, 0, self.message, bold=True)
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.height + self.y - 1,
|
||||
self.width + self.x - 1)
|
||||
|
@ -1,13 +1,17 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
from squirrelbattle.display.mapdisplay import MapDisplay
|
||||
from squirrelbattle.display.statsdisplay import StatsDisplay
|
||||
from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \
|
||||
MainMenuDisplay
|
||||
from squirrelbattle.display.logsdisplay import LogsDisplay
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from typing import Any
|
||||
from squirrelbattle.game import Game, GameMode
|
||||
from squirrelbattle.enums import DisplayActions
|
||||
from typing import Any, List
|
||||
|
||||
from .display import Display, HorizontalSplit, MessageDisplay, VerticalSplit
|
||||
from .gamedisplay import LogsDisplay, MapDisplay, StatsDisplay
|
||||
from .menudisplay import ChestInventoryDisplay, CreditsDisplay, \
|
||||
MainMenuDisplay, PlayerInventoryDisplay, \
|
||||
SettingsMenuDisplay, StoreInventoryDisplay
|
||||
from .texturepack import TexturePack
|
||||
from ..enums import DisplayActions
|
||||
from ..game import Game, GameMode
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
@ -18,47 +22,147 @@ class DisplayManager:
|
||||
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
|
||||
self.mapdisplay = MapDisplay(screen, pack)
|
||||
self.statsdisplay = StatsDisplay(screen, pack)
|
||||
self.logsdisplay = LogsDisplay(screen, pack)
|
||||
self.playerinventorydisplay = PlayerInventoryDisplay(screen, pack)
|
||||
self.storeinventorydisplay = StoreInventoryDisplay(screen, pack)
|
||||
self.chestinventorydisplay = ChestInventoryDisplay(screen, pack)
|
||||
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
|
||||
screen, pack)
|
||||
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
|
||||
self.logsdisplay = LogsDisplay(screen, pack)
|
||||
self.messagedisplay = MessageDisplay(screen, pack)
|
||||
self.hbar = HorizontalSplit(screen, pack)
|
||||
self.vbar = VerticalSplit(screen, pack)
|
||||
self.creditsdisplay = CreditsDisplay(screen, pack)
|
||||
self.displays = [self.statsdisplay, self.mapdisplay,
|
||||
self.mainmenudisplay, self.settingsmenudisplay,
|
||||
self.logsdisplay]
|
||||
self.logsdisplay, self.messagedisplay,
|
||||
self.playerinventorydisplay,
|
||||
self.storeinventorydisplay, self.creditsdisplay,
|
||||
self.chestinventorydisplay]
|
||||
self.update_game_components()
|
||||
|
||||
def handle_display_action(self, action: DisplayActions) -> None:
|
||||
def handle_display_action(self, action: DisplayActions, *params) -> None:
|
||||
"""
|
||||
Handles the differents values of display action.
|
||||
"""
|
||||
if action == DisplayActions.REFRESH:
|
||||
self.refresh()
|
||||
elif action == DisplayActions.UPDATE:
|
||||
self.update_game_components()
|
||||
elif action == DisplayActions.MOUSE:
|
||||
self.handle_mouse_click(*params)
|
||||
|
||||
def update_game_components(self) -> None:
|
||||
"""
|
||||
The game state was updated.
|
||||
Trigger all displays of these modifications.
|
||||
"""
|
||||
for d in self.displays:
|
||||
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
|
||||
self.mapdisplay.update_map(self.game.map)
|
||||
self.statsdisplay.update_player(self.game.player)
|
||||
self.settingsmenudisplay.update_menu(self.game.settings_menu)
|
||||
self.logsdisplay.update_logs(self.game.logs)
|
||||
d.update(self.game)
|
||||
|
||||
def refresh(self) -> None:
|
||||
if self.game.state == GameMode.PLAY:
|
||||
def handle_mouse_click(self, y: int, x: int, attr: int) -> None:
|
||||
"""
|
||||
Handles the mouse clicks.
|
||||
"""
|
||||
displays = self.refresh()
|
||||
display = None
|
||||
for d in displays:
|
||||
top_y, top_x, height, width = d.y, d.x, d.height, d.width
|
||||
if top_y <= y < top_y + height and top_x <= x < top_x + width:
|
||||
# The click coordinates correspond to the coordinates
|
||||
# of that display
|
||||
display = d
|
||||
if display:
|
||||
display.handle_click(y - display.y, x - display.x, attr, self.game)
|
||||
|
||||
def refresh(self) -> List[Display]:
|
||||
"""
|
||||
Refreshes all components on the screen.
|
||||
"""
|
||||
displays = []
|
||||
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
|
||||
|
||||
if self.game.state == GameMode.PLAY \
|
||||
or self.game.state == GameMode.INVENTORY \
|
||||
or self.game.state == GameMode.STORE\
|
||||
or self.game.state == GameMode.CHEST:
|
||||
# The map pad has already the good size
|
||||
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols,
|
||||
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5,
|
||||
self.mapdisplay.pack.tile_width
|
||||
* (self.cols * 4 // 5
|
||||
// self.mapdisplay.pack.tile_width),
|
||||
resize_pad=False)
|
||||
self.statsdisplay.refresh(self.rows * 4 // 5, 0,
|
||||
self.rows // 10, self.cols)
|
||||
self.logsdisplay.refresh(self.rows * 9 // 10, 0,
|
||||
self.rows // 10, self.cols)
|
||||
if self.game.state == GameMode.MAINMENU:
|
||||
self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1,
|
||||
self.rows, self.cols // 5 - 1)
|
||||
self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0,
|
||||
self.rows // 5 - 1, self.cols * 4 // 5)
|
||||
self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5)
|
||||
self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1)
|
||||
|
||||
displays += [self.mapdisplay, self.statsdisplay, self.logsdisplay,
|
||||
self.hbar, self.vbar]
|
||||
|
||||
if self.game.state == GameMode.INVENTORY:
|
||||
self.playerinventorydisplay.refresh(
|
||||
self.rows // 10,
|
||||
pack.tile_width * (self.cols // (2 * pack.tile_width)),
|
||||
8 * self.rows // 10,
|
||||
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
|
||||
displays.append(self.playerinventorydisplay)
|
||||
elif self.game.state == GameMode.STORE:
|
||||
self.storeinventorydisplay.refresh(
|
||||
self.rows // 10,
|
||||
pack.tile_width * (self.cols // (2 * pack.tile_width)),
|
||||
8 * self.rows // 10,
|
||||
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
|
||||
self.playerinventorydisplay.refresh(
|
||||
self.rows // 10,
|
||||
pack.tile_width * (self.cols // (10 * pack.tile_width)),
|
||||
8 * self.rows // 10,
|
||||
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
|
||||
displays.append(self.storeinventorydisplay)
|
||||
displays.append(self.playerinventorydisplay)
|
||||
elif self.game.state == GameMode.CHEST:
|
||||
self.chestinventorydisplay.refresh(
|
||||
self.rows // 10,
|
||||
pack.tile_width * (self.cols // (2 * pack.tile_width)),
|
||||
8 * self.rows // 10,
|
||||
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
|
||||
self.playerinventorydisplay.refresh(
|
||||
self.rows // 10,
|
||||
pack.tile_width * (self.cols // (10 * pack.tile_width)),
|
||||
8 * self.rows // 10,
|
||||
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
|
||||
displays.append(self.chestinventorydisplay)
|
||||
displays.append(self.playerinventorydisplay)
|
||||
elif self.game.state == GameMode.MAINMENU:
|
||||
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
|
||||
if self.game.state == GameMode.SETTINGS:
|
||||
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
|
||||
displays.append(self.mainmenudisplay)
|
||||
elif self.game.state == GameMode.SETTINGS:
|
||||
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
|
||||
displays.append(self.settingsmenudisplay)
|
||||
elif self.game.state == GameMode.CREDITS:
|
||||
self.creditsdisplay.refresh(0, 0, self.rows, self.cols)
|
||||
displays.append(self.creditsdisplay)
|
||||
|
||||
if self.game.message:
|
||||
height, width = 0, 0
|
||||
for line in self.game.message.split("\n"):
|
||||
height += 1
|
||||
width = max(width, len(line))
|
||||
y = pack.tile_width * (self.rows - height) // (2 * pack.tile_width)
|
||||
x = pack.tile_width * ((self.cols - width) // (2 * pack.tile_width))
|
||||
self.messagedisplay.refresh(y, x, height, width)
|
||||
displays.append(self.messagedisplay)
|
||||
|
||||
self.resize_window()
|
||||
|
||||
return displays
|
||||
|
||||
def resize_window(self) -> bool:
|
||||
"""
|
||||
If the window got resized, ensure that the screen size got updated.
|
||||
When the window is resized, ensures that the screen size is updated.
|
||||
"""
|
||||
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
|
||||
if self.screen and curses.is_term_resized(self.rows,
|
||||
@ -69,8 +173,16 @@ class DisplayManager:
|
||||
|
||||
@property
|
||||
def rows(self) -> int:
|
||||
"""
|
||||
Overwrites the native curses attribute of the same name,
|
||||
for testing purposes.
|
||||
"""
|
||||
return curses.LINES if self.screen else 42
|
||||
|
||||
@property
|
||||
def cols(self) -> int:
|
||||
"""
|
||||
Overwrites the native curses attribute of the same name,
|
||||
for testing purposes.
|
||||
"""
|
||||
return curses.COLS if self.screen else 42
|
||||
|
235
squirrelbattle/display/gamedisplay.py
Normal file
235
squirrelbattle/display/gamedisplay.py
Normal file
@ -0,0 +1,235 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
|
||||
from .display import Display
|
||||
from ..entities.items import Monocle
|
||||
from ..entities.player import Player
|
||||
from ..game import Game
|
||||
from ..interfaces import FightingEntity, Logs, Map
|
||||
from ..translations import gettext as _
|
||||
|
||||
|
||||
class LogsDisplay(Display):
|
||||
"""
|
||||
A class to handle the display of the logs.
|
||||
"""
|
||||
|
||||
logs: Logs
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
super().__init__(*args)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.logs = game.logs
|
||||
|
||||
def display(self) -> None:
|
||||
messages = self.logs.messages[-self.height:]
|
||||
messages = messages[::-1]
|
||||
self.pad.erase()
|
||||
for i in range(min(self.height, len(messages))):
|
||||
self.addstr(self.pad, self.height - i - 1, self.x,
|
||||
messages[i][:self.width])
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.y + self.height - 1, self.x + self.width - 1)
|
||||
|
||||
|
||||
class MapDisplay(Display):
|
||||
"""
|
||||
A class to handle the display of the map.
|
||||
"""
|
||||
|
||||
map: Map
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.map = game.map
|
||||
self.pad = self.newpad(self.map.height,
|
||||
self.pack.tile_width * self.map.width + 1)
|
||||
|
||||
def update_pad(self) -> None:
|
||||
for j in range(len(self.map.tiles)):
|
||||
for i in range(len(self.map.tiles[j])):
|
||||
if not self.map.seen_tiles[j][i]:
|
||||
continue
|
||||
fg, bg = self.map.tiles[j][i].visible_color(self.pack) if \
|
||||
self.map.visibility[j][i] else \
|
||||
self.map.tiles[j][i].hidden_color(self.pack)
|
||||
self.addstr(self.pad, j, self.pack.tile_width * i,
|
||||
self.map.tiles[j][i].char(self.pack), fg, bg)
|
||||
for e in self.map.entities:
|
||||
if self.map.visibility[e.y][e.x]:
|
||||
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
|
||||
self.pack[e.name.upper()],
|
||||
self.pack.entity_fg_color,
|
||||
self.pack.entity_bg_color)
|
||||
|
||||
# Display Path map for debug purposes
|
||||
# from squirrelbattle.entities.player import Player
|
||||
# players = [ p for p in self.map.entities if isinstance(p,Player) ]
|
||||
# player = players[0] if len(players) > 0 else None
|
||||
# if player:
|
||||
# for x in range(self.map.width):
|
||||
# for y in range(self.map.height):
|
||||
# if (y,x) in player.paths:
|
||||
# deltay, deltax = (y - player.paths[(y, x)][0],
|
||||
# x - player.paths[(y, x)][1])
|
||||
# if (deltay, deltax) == (-1, 0):
|
||||
# character = '↓'
|
||||
# elif (deltay, deltax) == (1, 0):
|
||||
# character = '↑'
|
||||
# elif (deltay, deltax) == (0, -1):
|
||||
# character = '→'
|
||||
# else:
|
||||
# character = '←'
|
||||
# self.addstr(self.pad, y, self.pack.tile_width * x,
|
||||
# character, self.pack.tile_fg_color,
|
||||
# self.pack.tile_bg_color)
|
||||
|
||||
def display(self) -> None:
|
||||
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
|
||||
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
|
||||
pminrow, pmincol = y - deltay, x - deltax
|
||||
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
|
||||
deltay, deltax = self.height - deltay, self.width - deltax
|
||||
smaxrow = self.map.height - (y + deltay) + self.height - 1
|
||||
smaxrow = min(smaxrow, self.height - 1)
|
||||
smaxcol = self.pack.tile_width * self.map.width - \
|
||||
(x + deltax) + self.width - 1
|
||||
|
||||
# Wrap perfectly the map according to the width of the tiles
|
||||
pmincol = self.pack.tile_width * (pmincol // self.pack.tile_width)
|
||||
smincol = self.pack.tile_width * (smincol // self.pack.tile_width)
|
||||
smaxcol = self.pack.tile_width \
|
||||
* (smaxcol // self.pack.tile_width + 1) - 1
|
||||
|
||||
smaxcol = min(smaxcol, self.width - 1)
|
||||
pminrow = max(0, min(self.map.height, pminrow))
|
||||
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
|
||||
|
||||
self.pad.erase()
|
||||
self.update_pad()
|
||||
self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow,
|
||||
smaxcol)
|
||||
|
||||
|
||||
class StatsDisplay(Display):
|
||||
"""
|
||||
A class to handle the display of the stats of the player.
|
||||
"""
|
||||
game: Game
|
||||
player: Player
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.game = game
|
||||
self.player = game.player
|
||||
|
||||
def update_pad(self) -> None:
|
||||
string2 = f"{_(self.player.name).capitalize()} " \
|
||||
f"-- LVL {self.player.level} -- " \
|
||||
f"FLOOR {-self.player.map.floor}\n" \
|
||||
f"EXP {self.player.current_xp}/{self.player.max_xp}\n" \
|
||||
f"HP {self.player.health}/{self.player.maxhealth}"
|
||||
self.addstr(self.pad, 0, 0, string2)
|
||||
string3 = f"STR {self.player.strength}\n" \
|
||||
f"INT {self.player.intelligence}\n" \
|
||||
f"CHR {self.player.charisma}\n" \
|
||||
f"DEX {self.player.dexterity}\n" \
|
||||
f"CON {self.player.constitution}\n" \
|
||||
f"CRI {self.player.critical}%"
|
||||
self.addstr(self.pad, 3, 0, string3)
|
||||
|
||||
inventory_str = _("Inventory:") + " "
|
||||
# Stack items by type instead of displaying each item
|
||||
item_types = [item.name for item in self.player.inventory]
|
||||
item_types.sort(key=item_types.count, reverse=True)
|
||||
printed_items = []
|
||||
for item in item_types:
|
||||
if item in printed_items:
|
||||
continue
|
||||
count = item_types.count(item)
|
||||
inventory_str += self.pack[item.upper()]
|
||||
if count > 1:
|
||||
inventory_str += f"x{count} "
|
||||
printed_items.append(item)
|
||||
self.addstr(self.pad, 9, 0, inventory_str)
|
||||
|
||||
if self.player.equipped_main:
|
||||
self.addstr(self.pad, 10, 0,
|
||||
_("Equipped main:") + " "
|
||||
f"{self.pack[self.player.equipped_main.name.upper()]}")
|
||||
if self.player.equipped_secondary:
|
||||
self.addstr(self.pad, 11, 0,
|
||||
_("Equipped secondary:") + " "
|
||||
+ self.pack[self.player.equipped_secondary
|
||||
.name.upper()])
|
||||
if self.player.equipped_armor:
|
||||
self.addstr(self.pad, 12, 0,
|
||||
_("Equipped chestplate:") + " "
|
||||
+ self.pack[self.player.equipped_armor.name.upper()])
|
||||
if self.player.equipped_helmet:
|
||||
self.addstr(self.pad, 13, 0,
|
||||
_("Equipped helmet:") + " "
|
||||
+ self.pack[self.player.equipped_helmet.name.upper()])
|
||||
|
||||
self.addstr(self.pad, 14, 0, f"{self.pack.HAZELNUT} "
|
||||
f"x{self.player.hazel}")
|
||||
|
||||
if self.player.dead:
|
||||
self.addstr(self.pad, 15, 0, _("YOU ARE DEAD"), curses.COLOR_RED,
|
||||
bold=True, blink=True, standout=True)
|
||||
|
||||
if self.player.map.tiles[self.player.y][self.player.x].is_ladder():
|
||||
msg = _("Use {key} to use the ladder") \
|
||||
.format(key=self.game.settings.KEY_LADDER.upper())
|
||||
self.addstr(self.pad, self.height - 2, 0, msg,
|
||||
italic=True, reverse=True)
|
||||
|
||||
self.update_entities_stats()
|
||||
|
||||
def update_entities_stats(self) -> None:
|
||||
"""
|
||||
Display information about a near entity if we have a monocle.
|
||||
"""
|
||||
for dy, dx in [(-1, 0), (0, -1), (0, 1), (1, 0)]:
|
||||
for entity in self.player.map.find_entities(FightingEntity):
|
||||
if entity == self.player:
|
||||
continue
|
||||
|
||||
if entity.y == self.player.y + dy \
|
||||
and entity.x == self.player.x + dx:
|
||||
if entity.is_friendly():
|
||||
msg = _("Move to the friendly entity to talk to it") \
|
||||
if self.game.waiting_for_friendly_key else \
|
||||
_("Use {key} then move to talk to the entity") \
|
||||
.format(key=self.game.settings.KEY_CHAT.upper())
|
||||
self.addstr(self.pad, self.height - 1, 0, msg,
|
||||
italic=True, reverse=True)
|
||||
|
||||
if isinstance(self.player.equipped_secondary, Monocle):
|
||||
# Truth monocle
|
||||
message = f"{entity.translated_name.capitalize()} " \
|
||||
f"{self.pack[entity.name.upper()]}\n" \
|
||||
f"STR {entity.strength}\n" \
|
||||
f"INT {entity.intelligence}\n" \
|
||||
f"CHR {entity.charisma}\n" \
|
||||
f"DEX {entity.dexterity}\n" \
|
||||
f"CON {entity.constitution}\n" \
|
||||
f"CRI {entity.critical}%"
|
||||
self.addstr(self.pad, 17, 0, message)
|
||||
# Only display one entity
|
||||
break
|
||||
|
||||
def display(self) -> None:
|
||||
self.pad.erase()
|
||||
self.update_pad()
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.y + self.height - 1, self.width + self.x - 1)
|
@ -1,23 +0,0 @@
|
||||
from squirrelbattle.display.display import Display
|
||||
from squirrelbattle.interfaces import Logs
|
||||
|
||||
|
||||
class LogsDisplay(Display):
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
super().__init__(*args)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
|
||||
def update_logs(self, logs: Logs) -> None:
|
||||
self.logs = logs
|
||||
|
||||
def display(self) -> None:
|
||||
print(type(self.logs.messages), flush=True)
|
||||
messages = self.logs.messages[-self.height:]
|
||||
messages = messages[::-1]
|
||||
self.pad.clear()
|
||||
for i in range(min(self.height, len(messages))):
|
||||
self.pad.addstr(self.height - i - 1, self.x,
|
||||
messages[i][:self.width])
|
||||
self.pad.refresh(0, 0, self.y, self.x, self.y + self.height,
|
||||
self.x + self.width)
|
@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from squirrelbattle.interfaces import Map
|
||||
from .display import Display
|
||||
|
||||
|
||||
class MapDisplay(Display):
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def update_map(self, m: Map) -> None:
|
||||
self.map = m
|
||||
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
|
||||
|
||||
def update_pad(self) -> None:
|
||||
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
|
||||
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
|
||||
self.pad.addstr(0, 0, self.map.draw_string(self.pack),
|
||||
self.color_pair(1))
|
||||
for e in self.map.entities:
|
||||
self.pad.addstr(e.y, self.pack.tile_width * e.x,
|
||||
self.pack[e.name.upper()], self.color_pair(2))
|
||||
|
||||
def display(self) -> None:
|
||||
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
|
||||
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
|
||||
pminrow, pmincol = y - deltay, x - deltax
|
||||
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
|
||||
deltay, deltax = self.height - deltay, self.width - deltax
|
||||
smaxrow = self.map.height - (y + deltay) + self.height - 1
|
||||
smaxrow = min(smaxrow, self.height - 1)
|
||||
smaxcol = self.pack.tile_width * self.map.width - \
|
||||
(x + deltax) + self.width - 1
|
||||
smaxcol = min(smaxcol, self.width - 1)
|
||||
pminrow = max(0, min(self.map.height, pminrow))
|
||||
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
|
||||
self.pad.clear()
|
||||
self.update_pad()
|
||||
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)
|
@ -1,32 +1,41 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
from random import randint
|
||||
from typing import List
|
||||
|
||||
from squirrelbattle.menus import Menu, MainMenu
|
||||
from .display import Display
|
||||
from .display import Box, Display
|
||||
from ..entities.player import Player
|
||||
from ..enums import GameMode, KeyValues
|
||||
from ..game import Game
|
||||
from ..menus import ChestMenu, MainMenu, Menu, SettingsMenu, StoreMenu
|
||||
from ..resources import ResourceManager
|
||||
from ..translations import gettext as _
|
||||
|
||||
|
||||
class MenuDisplay(Display):
|
||||
"""
|
||||
A class to display the menu objects.
|
||||
"""
|
||||
menu: Menu
|
||||
position: int
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.menubox = self.newpad(self.rows, self.cols)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.menubox = Box(*args, **kwargs)
|
||||
|
||||
def update_menu(self, menu: Menu) -> None:
|
||||
self.menu = menu
|
||||
self.trueheight = len(self.values)
|
||||
self.truewidth = max([len(a) for a in self.values])
|
||||
|
||||
# Menu values are printed in pad
|
||||
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
|
||||
for i in range(self.trueheight):
|
||||
self.pad.addstr(i, 0, " " + self.values[i])
|
||||
|
||||
def update_pad(self) -> None:
|
||||
for i in range(self.trueheight):
|
||||
self.pad.addstr(i, 0, " " + self.values[i])
|
||||
self.addstr(self.pad, i, 0, " " + self.values[i])
|
||||
# set a marker on the selected line
|
||||
self.pad.addstr(self.menu.position, 0, ">")
|
||||
self.addstr(self.pad, self.menu.position, 0, " >")
|
||||
|
||||
def display(self) -> None:
|
||||
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
|
||||
@ -34,20 +43,28 @@ class MenuDisplay(Display):
|
||||
if self.height - 2 >= self.trueheight - self.menu.position else 0
|
||||
|
||||
# Menu box
|
||||
self.menubox.addstr(0, 0, "┏" + "━" * (self.width - 2) + "┓")
|
||||
for i in range(1, self.height - 1):
|
||||
self.menubox.addstr(i, 0, "┃" + " " * (self.width - 2) + "┃")
|
||||
self.menubox.addstr(self.height - 1, 0,
|
||||
"┗" + "━" * (self.width - 2) + "┛")
|
||||
|
||||
self.menubox.refresh(0, 0, self.y, self.x,
|
||||
self.height + self.y,
|
||||
self.width + self.x)
|
||||
self.menubox.refresh(self.y, self.x, self.height, self.width)
|
||||
self.pad.erase()
|
||||
self.update_pad()
|
||||
self.pad.refresh(cornery, 0, self.y + 1, self.x + 2,
|
||||
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 1,
|
||||
self.height - 2 + self.y,
|
||||
self.width - 2 + self.x)
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
"""
|
||||
We can select a menu item with the mouse.
|
||||
"""
|
||||
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 1))
|
||||
game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
@property
|
||||
def truewidth(self) -> int:
|
||||
return max([len(str(a)) for a in self.values])
|
||||
|
||||
@property
|
||||
def trueheight(self) -> int:
|
||||
return len(self.values)
|
||||
|
||||
@property
|
||||
def preferred_width(self) -> int:
|
||||
return self.truewidth + 6
|
||||
@ -62,35 +79,296 @@ class MenuDisplay(Display):
|
||||
|
||||
|
||||
class SettingsMenuDisplay(MenuDisplay):
|
||||
"""
|
||||
A class to display specifically a settingsmenu object.
|
||||
"""
|
||||
menu: SettingsMenu
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.update_menu(game.settings_menu)
|
||||
|
||||
@property
|
||||
def values(self) -> List[str]:
|
||||
return [a[1][1] + (" : "
|
||||
return [_(a[1][1]) + (" : "
|
||||
+ ("?" if self.menu.waiting_for_key
|
||||
and a == self.menu.validate() else a[1][0])
|
||||
and a == self.menu.validate() else a[1][0]
|
||||
.replace("\n", "\\n"))
|
||||
if a[1][0] else "") for a in self.menu.values]
|
||||
|
||||
|
||||
class MainMenuDisplay(Display):
|
||||
"""
|
||||
A class to display specifically a mainmenu object.
|
||||
"""
|
||||
def __init__(self, menu: MainMenu, *args):
|
||||
super().__init__(*args)
|
||||
self.menu = menu
|
||||
|
||||
with open(ResourceManager.get_asset_path("ascii_art.txt"), "r") as file:
|
||||
with open(ResourceManager.get_asset_path("ascii_art-title.txt"), "r")\
|
||||
as file:
|
||||
self.title = file.read().split("\n")
|
||||
|
||||
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
|
||||
max(len(self.title[0]) + 5, self.cols))
|
||||
|
||||
self.fg_color = curses.COLOR_WHITE
|
||||
|
||||
self.menudisplay = MenuDisplay(self.screen, self.pack)
|
||||
self.menudisplay.update_menu(self.menu)
|
||||
|
||||
def display(self) -> None:
|
||||
for i in range(len(self.title)):
|
||||
self.pad.addstr(4 + i, max(self.width // 2
|
||||
- len(self.title[0]) // 2 - 1, 0), self.title[i])
|
||||
self.pad.refresh(0, 0, self.y, self.x, self.height, self.width)
|
||||
self.addstr(self.pad, 4 + i, max(self.width // 2
|
||||
- len(self.title[0]) // 2 - 1, 0), self.title[i],
|
||||
self.fg_color)
|
||||
msg = _("Credits")
|
||||
self.addstr(self.pad, self.height - 1, self.width - 1 - len(msg), msg)
|
||||
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
|
||||
self.height + self.y - 1,
|
||||
self.width + self.x - 1)
|
||||
menuwidth = min(self.menudisplay.preferred_width, self.width)
|
||||
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
|
||||
self.menudisplay.refresh(
|
||||
menuy, menux, min(self.menudisplay.preferred_height,
|
||||
self.height - menuy), menuwidth)
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.menudisplay.update_menu(game.main_menu)
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
menuwidth = min(self.menudisplay.preferred_width, self.width)
|
||||
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
|
||||
menuheight = min(self.menudisplay.preferred_height, self.height - menuy)
|
||||
|
||||
if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth:
|
||||
self.menudisplay.handle_click(y - menuy, x - menux, attr, game)
|
||||
|
||||
if y <= len(self.title):
|
||||
self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
|
||||
|
||||
if y == self.height - 1 and x >= self.width - 1 - len(_("Credits")):
|
||||
game.state = GameMode.CREDITS
|
||||
|
||||
|
||||
class PlayerInventoryDisplay(MenuDisplay):
|
||||
"""
|
||||
A class to handle the display of the player's inventory.
|
||||
"""
|
||||
player: Player = None
|
||||
selected: bool = True
|
||||
store_mode: bool = False
|
||||
chest_mode: bool = False
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.player = game.player
|
||||
self.update_menu(game.inventory_menu)
|
||||
game.inventory_menu.update_player(self.player)
|
||||
self.store_mode = game.state == GameMode.STORE
|
||||
self.chest_mode = game.state == GameMode.CHEST
|
||||
self.selected = game.state == GameMode.INVENTORY \
|
||||
or (self.store_mode and not game.is_in_store_menu)\
|
||||
or (self.chest_mode and not game.is_in_chest_menu)
|
||||
|
||||
def update_pad(self) -> None:
|
||||
self.menubox.update_title(_("INVENTORY"))
|
||||
for i, item in enumerate(self.menu.values):
|
||||
rep = self.pack[item.name.upper()]
|
||||
selection = f"[{rep}]" if i == self.menu.position \
|
||||
and self.selected else f" {rep} "
|
||||
self.addstr(self.pad, i + 1, 0, selection
|
||||
+ " " + ("[E]" if item.equipped else "")
|
||||
+ item.translated_name.capitalize()
|
||||
+ (f" ({item.description})" if item.description else "")
|
||||
+ (": " + str(item.price) + " Hazels"
|
||||
if self.store_mode else ""))
|
||||
|
||||
if self.store_mode:
|
||||
price = f"{self.pack.HAZELNUT} {self.player.hazel} Hazels"
|
||||
width = len(price) + (self.pack.tile_width - 1)
|
||||
self.addstr(self.pad, self.height - 3, self.width - width - 2,
|
||||
price, italic=True)
|
||||
|
||||
@property
|
||||
def truewidth(self) -> int:
|
||||
return max(1, self.height if hasattr(self, "height") else 10)
|
||||
|
||||
@property
|
||||
def trueheight(self) -> int:
|
||||
return 2 + super().trueheight
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
"""
|
||||
We can select a menu item with the mouse.
|
||||
"""
|
||||
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
|
||||
game.is_in_store_menu = False
|
||||
game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
|
||||
class StoreInventoryDisplay(MenuDisplay):
|
||||
"""
|
||||
A class to handle the display of a merchant's inventory.
|
||||
"""
|
||||
menu: StoreMenu
|
||||
selected: bool = False
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.update_menu(game.store_menu)
|
||||
self.selected = game.is_in_store_menu
|
||||
|
||||
def update_pad(self) -> None:
|
||||
self.menubox.update_title(_("STALL"))
|
||||
for i, item in enumerate(self.menu.values):
|
||||
rep = self.pack[item.name.upper()]
|
||||
selection = f"[{rep}]" if i == self.menu.position \
|
||||
and self.selected else f" {rep} "
|
||||
self.addstr(self.pad, i + 1, 0, selection
|
||||
+ " " + item.translated_name.capitalize()
|
||||
+ (f" ({item.description})" if item.description else "")
|
||||
+ ": " + str(item.price) + " Hazels")
|
||||
|
||||
price = f"{self.pack.HAZELNUT} {self.menu.merchant.hazel} Hazels"
|
||||
width = len(price) + (self.pack.tile_width - 1)
|
||||
self.addstr(self.pad, self.height - 3, self.width - width - 2, price,
|
||||
italic=True)
|
||||
|
||||
@property
|
||||
def truewidth(self) -> int:
|
||||
return max(1, self.height if hasattr(self, "height") else 10)
|
||||
|
||||
@property
|
||||
def trueheight(self) -> int:
|
||||
return 2 + super().trueheight
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
"""
|
||||
We can select a menu item with the mouse.
|
||||
"""
|
||||
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
|
||||
game.is_in_store_menu = True
|
||||
game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
|
||||
class ChestInventoryDisplay(MenuDisplay):
|
||||
"""
|
||||
A class to handle the display of a merchant's inventory.
|
||||
"""
|
||||
menu: ChestMenu
|
||||
selected: bool = False
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
self.update_menu(game.chest_menu)
|
||||
self.selected = game.is_in_chest_menu
|
||||
|
||||
def update_pad(self) -> None:
|
||||
self.menubox.update_title(_("CHEST"))
|
||||
for i, item in enumerate(self.menu.values):
|
||||
rep = self.pack[item.name.upper()]
|
||||
selection = f"[{rep}]" if i == self.menu.position \
|
||||
and self.selected else f" {rep} "
|
||||
self.addstr(self.pad, i + 1, 0, selection
|
||||
+ " " + item.translated_name.capitalize())
|
||||
|
||||
@property
|
||||
def truewidth(self) -> int:
|
||||
return max(1, self.height if hasattr(self, "height") else 10)
|
||||
|
||||
@property
|
||||
def trueheight(self) -> int:
|
||||
return 2 + super().trueheight
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
"""
|
||||
We can select a menu item with the mouse.
|
||||
"""
|
||||
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
|
||||
game.is_in_chest_menu = True
|
||||
game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
|
||||
class CreditsDisplay(Display):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.box = Box(*args, **kwargs)
|
||||
self.pad = self.newpad(1, 1)
|
||||
self.ascii_art_displayed = False
|
||||
|
||||
def update(self, game: Game) -> None:
|
||||
return
|
||||
|
||||
def display(self) -> None:
|
||||
self.box.refresh(self.y, self.x, self.height, self.width)
|
||||
self.box.display()
|
||||
self.pad.erase()
|
||||
|
||||
messages = [
|
||||
_("Credits"),
|
||||
"",
|
||||
"Squirrel Battle",
|
||||
"",
|
||||
_("Developers:"),
|
||||
"Yohann \"ÿnérant\" D'ANELLO",
|
||||
"Mathilde \"eichhornchen\" DÉPRÉS",
|
||||
"Nicolas \"nicomarg\" MARGULIES",
|
||||
"Charles \"charsle\" PEYRAT",
|
||||
"",
|
||||
_("Translators:"),
|
||||
"Hugo \"ifugao\" JACOB (español)",
|
||||
]
|
||||
|
||||
for i, msg in enumerate(messages):
|
||||
self.addstr(self.pad, i + (self.height - len(messages)) // 2,
|
||||
(self.width - len(msg)) // 2, msg,
|
||||
bold=(i == 0), italic=(":" in msg))
|
||||
|
||||
if self.ascii_art_displayed:
|
||||
self.display_ascii_art()
|
||||
|
||||
self.refresh_pad(self.pad, 0, 0, self.y + 1, self.x + 1,
|
||||
self.height + self.y - 2,
|
||||
self.width + self.x - 2)
|
||||
|
||||
def display_ascii_art(self) -> None:
|
||||
with open(ResourceManager.get_asset_path("ascii-art-ecureuil.txt"))\
|
||||
as f:
|
||||
ascii_art = f.read().split("\n")
|
||||
|
||||
height, width = len(ascii_art), len(ascii_art[0])
|
||||
y_offset, x_offset = (self.height - height) // 2,\
|
||||
(self.width - width) // 2
|
||||
|
||||
for i, line in enumerate(ascii_art):
|
||||
for j, c in enumerate(line):
|
||||
bg_color = curses.COLOR_WHITE
|
||||
fg_color = curses.COLOR_BLACK
|
||||
bold = False
|
||||
if c == ' ':
|
||||
bg_color = curses.COLOR_BLACK
|
||||
elif c == '━' or c == '┃' or c == '⋀':
|
||||
bold = True
|
||||
fg_color = curses.COLOR_WHITE
|
||||
bg_color = curses.COLOR_BLACK
|
||||
elif c == '|':
|
||||
bold = True # c = '┃'
|
||||
fg_color = (100, 700, 1000)
|
||||
bg_color = curses.COLOR_BLACK
|
||||
elif c == '▓':
|
||||
fg_color = (700, 300, 0)
|
||||
elif c == '▒':
|
||||
fg_color = (700, 300, 0)
|
||||
bg_color = curses.COLOR_BLACK
|
||||
elif c == '░':
|
||||
fg_color = (350, 150, 0)
|
||||
elif c == '█':
|
||||
fg_color = (0, 0, 0)
|
||||
bg_color = curses.COLOR_BLACK
|
||||
elif c == '▬':
|
||||
c = '█'
|
||||
fg_color = (1000, 1000, 1000)
|
||||
bg_color = curses.COLOR_BLACK
|
||||
self.addstr(self.pad, y_offset + i, x_offset + j, c,
|
||||
fg_color, bg_color, bold=bold)
|
||||
|
||||
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
|
||||
if self.pad.inch(y - 1, x - 1) != ord(" "):
|
||||
self.ascii_art_displayed = True
|
||||
|
@ -1,52 +0,0 @@
|
||||
import curses
|
||||
|
||||
from .display import Display
|
||||
|
||||
from squirrelbattle.entities.player import Player
|
||||
|
||||
|
||||
class StatsDisplay(Display):
|
||||
player: Player
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad = self.newpad(self.rows, self.cols)
|
||||
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
|
||||
def update_player(self, p: Player) -> None:
|
||||
self.player = p
|
||||
|
||||
def update_pad(self) -> None:
|
||||
string = ""
|
||||
for _ in range(self.width - 1):
|
||||
string = string + "-"
|
||||
self.pad.addstr(0, 0, string)
|
||||
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
|
||||
.format(self.player.level, self.player.current_xp,
|
||||
self.player.max_xp, self.player.health,
|
||||
self.player.maxhealth)
|
||||
for _ in range(self.width - len(string2) - 1):
|
||||
string2 = string2 + " "
|
||||
self.pad.addstr(1, 0, string2)
|
||||
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
|
||||
.format(self.player.strength,
|
||||
self.player.intelligence, self.player.charisma,
|
||||
self.player.dexterity, self.player.constitution)
|
||||
for _ in range(self.width - len(string3) - 1):
|
||||
string3 = string3 + " "
|
||||
self.pad.addstr(2, 0, string3)
|
||||
|
||||
inventory_str = "Inventaire : " + "".join(
|
||||
self.pack[item.name.upper()] for item in self.player.inventory)
|
||||
self.pad.addstr(3, 0, inventory_str)
|
||||
|
||||
if self.player.dead:
|
||||
self.pad.addstr(4, 0, "VOUS ÊTES MORT",
|
||||
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
|
||||
| self.color_pair(3))
|
||||
|
||||
def display(self) -> None:
|
||||
self.pad.clear()
|
||||
self.update_pad()
|
||||
self.pad.refresh(0, 0, self.y, self.x,
|
||||
4 + self.y, self.width + self.x)
|
@ -1,20 +1,52 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
from typing import Any
|
||||
from typing import Any, Tuple, Union
|
||||
|
||||
|
||||
class TexturePack:
|
||||
"""
|
||||
A class to handle displaying several textures.
|
||||
"""
|
||||
_packs = dict()
|
||||
|
||||
name: str
|
||||
tile_width: int
|
||||
tile_fg_color: int
|
||||
tile_bg_color: int
|
||||
entity_fg_color: int
|
||||
entity_bg_color: int
|
||||
tile_fg_color: Union[int, Tuple[int, int, int]]
|
||||
tile_fg_visible_color: Union[int, Tuple[int, int, int]]
|
||||
tile_bg_color: Union[int, Tuple[int, int, int]]
|
||||
entity_fg_color: Union[int, Tuple[int, int, int]]
|
||||
entity_bg_color: Union[int, Tuple[int, int, int]]
|
||||
|
||||
BODY_SNATCH_POTION: str
|
||||
BOMB: str
|
||||
BOW: str
|
||||
CHEST: str
|
||||
CHESTPLATE: str
|
||||
EAGLE: str
|
||||
EMPTY: str
|
||||
WALL: str
|
||||
FIRE_BALL_STAFF: str
|
||||
FLOOR: str
|
||||
HAZELNUT: str
|
||||
HEART: str
|
||||
HEDGEHOG: str
|
||||
HELMET: str
|
||||
MERCHANT: str
|
||||
PLAYER: str
|
||||
RABBIT: str
|
||||
RING_OF_CRITICAL_DAMAGE: str
|
||||
RING_OF_MORE_EXPERIENCE: str
|
||||
RULER: str
|
||||
SCROLL_OF_DAMAGE: str
|
||||
SCROLL_OF_WEAKENING: str
|
||||
SHIELD: str
|
||||
SUNFLOWER: str
|
||||
SWORD: str
|
||||
TEDDY_BEAR: str
|
||||
TIGER: str
|
||||
TRUMPET: str
|
||||
WALL: str
|
||||
|
||||
ASCII_PACK: "TexturePack"
|
||||
SQUIRREL_PACK: "TexturePack"
|
||||
@ -39,37 +71,87 @@ class TexturePack:
|
||||
TexturePack.ASCII_PACK = TexturePack(
|
||||
name="ascii",
|
||||
tile_width=1,
|
||||
tile_fg_visible_color=(1000, 1000, 1000),
|
||||
tile_fg_color=curses.COLOR_WHITE,
|
||||
tile_bg_color=curses.COLOR_BLACK,
|
||||
entity_fg_color=curses.COLOR_WHITE,
|
||||
entity_fg_color=(1000, 1000, 1000),
|
||||
entity_bg_color=curses.COLOR_BLACK,
|
||||
|
||||
BODY_SNATCH_POTION='S',
|
||||
BOMB='ç',
|
||||
BOW=')',
|
||||
CHEST='□',
|
||||
CHESTPLATE='(',
|
||||
DOOR='&',
|
||||
EAGLE='µ',
|
||||
EMPTY=' ',
|
||||
WALL='#',
|
||||
EXPLOSION='%',
|
||||
FIRE_BALL_STAFF=':',
|
||||
FLOOR='.',
|
||||
PLAYER='@',
|
||||
HEDGEHOG='*',
|
||||
LADDER='H',
|
||||
HAZELNUT='¤',
|
||||
HEART='❤',
|
||||
BOMB='o',
|
||||
HEDGEHOG='*',
|
||||
HELMET='0',
|
||||
MERCHANT='M',
|
||||
MONOCLE='ô',
|
||||
PLAYER='@',
|
||||
RABBIT='Y',
|
||||
BEAVER='_',
|
||||
RING_OF_CRITICAL_DAMAGE='o',
|
||||
RING_OF_MORE_EXPERIENCE='o',
|
||||
RULER='\\',
|
||||
SHIELD='D',
|
||||
SUNFLOWER='I',
|
||||
SWORD='\u2020',
|
||||
TEDDY_BEAR='8',
|
||||
TIGER='n',
|
||||
TRUMPET='/',
|
||||
WALL='#',
|
||||
SCROLL_OF_DAMAGE=']',
|
||||
SCROLL_OF_WEAKENING=']',
|
||||
)
|
||||
|
||||
TexturePack.SQUIRREL_PACK = TexturePack(
|
||||
name="squirrel",
|
||||
tile_width=2,
|
||||
tile_fg_visible_color=(1000, 1000, 1000),
|
||||
tile_fg_color=curses.COLOR_WHITE,
|
||||
tile_bg_color=curses.COLOR_BLACK,
|
||||
entity_fg_color=curses.COLOR_WHITE,
|
||||
entity_bg_color=curses.COLOR_WHITE,
|
||||
EMPTY=' ',
|
||||
WALL='🧱',
|
||||
FLOOR='██',
|
||||
PLAYER='🐿 ️',
|
||||
HEDGEHOG='🦔',
|
||||
HEART='💜',
|
||||
entity_fg_color=(1000, 1000, 1000),
|
||||
entity_bg_color=(1000, 1000, 1000),
|
||||
|
||||
BODY_SNATCH_POTION='🔀',
|
||||
BOMB='💣',
|
||||
BOW='🏹',
|
||||
CHEST='🧰',
|
||||
CHESTPLATE='🦺',
|
||||
DOOR=('🚪', curses.COLOR_WHITE, (1000, 1000, 1000),
|
||||
curses.COLOR_WHITE, (1000, 1000, 1000)),
|
||||
EAGLE='🦅',
|
||||
EMPTY=' ',
|
||||
EXPLOSION='💥',
|
||||
FIRE_BALL_STAFF='🪄',
|
||||
FLOOR='██',
|
||||
LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000),
|
||||
curses.COLOR_WHITE, (1000, 1000, 1000)),
|
||||
HAZELNUT='🌰',
|
||||
HEART='💜',
|
||||
HEDGEHOG='🦔',
|
||||
HELMET='⛑️ ',
|
||||
PLAYER='🐿️ ️',
|
||||
MERCHANT='🦜',
|
||||
MONOCLE='🧐',
|
||||
RABBIT='🐇',
|
||||
BEAVER='🦫',
|
||||
RING_OF_CRITICAL_DAMAGE='💍',
|
||||
RING_OF_MORE_EXPERIENCE='💍',
|
||||
RULER='📏',
|
||||
SHIELD='🛡️ ',
|
||||
SUNFLOWER='🌻',
|
||||
SWORD='🗡️ ',
|
||||
TEDDY_BEAR='🧸',
|
||||
TIGER='🐅',
|
||||
TRUMPET='🎺',
|
||||
WALL='🧱',
|
||||
SCROLL_OF_DAMAGE='📜',
|
||||
SCROLL_OF_WEAKENING='📜',
|
||||
)
|
||||
|
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
178
squirrelbattle/entities/friendly.py
Normal file
178
squirrelbattle/entities/friendly.py
Normal file
@ -0,0 +1,178 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import choice, shuffle
|
||||
|
||||
from .items import Bomb, Item
|
||||
from .monsters import Monster
|
||||
from .player import Player
|
||||
from ..interfaces import Entity, FightingEntity, FriendlyEntity, \
|
||||
InventoryHolder, Map
|
||||
from ..translations import gettext as _
|
||||
|
||||
|
||||
class Merchant(InventoryHolder, FriendlyEntity):
|
||||
"""
|
||||
The class of merchants in the dungeon.
|
||||
"""
|
||||
def keys(self) -> list:
|
||||
"""
|
||||
Returns a friendly entitie's specific attributes.
|
||||
"""
|
||||
return super().keys() + ["inventory", "hazel"]
|
||||
|
||||
def __init__(self, name: str = "merchant", inventory: list = None,
|
||||
hazel: int = 75, maxhealth: int = 8, *args, **kwargs):
|
||||
super().__init__(name=name, maxhealth=maxhealth, *args, **kwargs)
|
||||
self.inventory = self.translate_inventory(inventory) \
|
||||
if inventory is not None else None
|
||||
self.hazel = hazel
|
||||
if self.inventory is None:
|
||||
self.inventory = []
|
||||
for i in range(5):
|
||||
self.inventory.append(choice(Item.get_all_items())())
|
||||
|
||||
def talk_to(self, player: Player) -> str:
|
||||
"""
|
||||
This function is used to open the merchant's inventory in a menu,
|
||||
and allows the player to buy/sell objects.
|
||||
"""
|
||||
return _("I don't sell any squirrel")
|
||||
|
||||
def change_hazel_balance(self, hz: int) -> None:
|
||||
"""
|
||||
Changes the number of hazel the merchant has by hz.
|
||||
"""
|
||||
self.hazel += hz
|
||||
|
||||
|
||||
class Chest(InventoryHolder, FriendlyEntity):
|
||||
"""
|
||||
A class of chest inanimate entities which contain objects.
|
||||
"""
|
||||
annihilated: bool
|
||||
|
||||
def __init__(self, name: str = "chest", inventory: list = None,
|
||||
hazel: int = 0, *args, **kwargs):
|
||||
super().__init__(name=name, *args, **kwargs)
|
||||
self.hazel = hazel
|
||||
self.inventory = self.translate_inventory(inventory) \
|
||||
if inventory is not None else None
|
||||
self.annihilated = False
|
||||
if self.inventory is None:
|
||||
self.inventory = []
|
||||
for i in range(3):
|
||||
self.inventory.append(choice(Item.get_all_items())())
|
||||
|
||||
def talk_to(self, player: Player) -> str:
|
||||
"""
|
||||
This function is used to open the chest's inventory in a menu,
|
||||
and allows the player to take objects.
|
||||
"""
|
||||
return _("You have opened the chest")
|
||||
|
||||
def take_damage(self, attacker: Entity, amount: int) -> str:
|
||||
"""
|
||||
A chest is not living, it can not take damage
|
||||
"""
|
||||
if isinstance(attacker, Bomb):
|
||||
self.die()
|
||||
self.annihilated = True
|
||||
return _("The chest exploded")
|
||||
return _("It's not really effective")
|
||||
|
||||
@property
|
||||
def dead(self) -> bool:
|
||||
"""
|
||||
Chest can not die
|
||||
"""
|
||||
return self.annihilated
|
||||
|
||||
|
||||
class Sunflower(FriendlyEntity):
|
||||
"""
|
||||
A friendly sunflower.
|
||||
"""
|
||||
def __init__(self, maxhealth: int = 20,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="sunflower", maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def dialogue_option(self) -> list:
|
||||
"""
|
||||
Lists all that a sunflower can say to the player.
|
||||
"""
|
||||
return [_("Flower power!!"), _("The sun is warm today")]
|
||||
|
||||
|
||||
class Familiar(FightingEntity):
|
||||
"""
|
||||
A friendly familiar that helps the player defeat monsters.
|
||||
"""
|
||||
def __init__(self, maxhealth: int = 25,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(maxhealth=maxhealth, *args, **kwargs)
|
||||
self.target = None
|
||||
|
||||
# @property
|
||||
# def dialogue_option(self) -> list:
|
||||
# """
|
||||
# Debug function (to see if used in the real game)
|
||||
# """
|
||||
# return [_("My target is"+str(self.target))]
|
||||
|
||||
def act(self, p: Player, m: Map) -> None:
|
||||
"""
|
||||
By default, the familiar tries to stay at distance at most 2 of the
|
||||
player and if a monster comes in range 3, it focuses on the monster
|
||||
and attacks it.
|
||||
"""
|
||||
if self.target is None:
|
||||
# If the previous target is dead(or if there was no previous target)
|
||||
# the familiar tries to get closer to the player.
|
||||
self.target = p
|
||||
elif self.target.dead:
|
||||
self.target = p
|
||||
if self.target == p:
|
||||
# Look for monsters around the player to kill TOFIX : if monster is
|
||||
# out of range, continue targetting player.
|
||||
for entity in m.entities:
|
||||
if (p.y - entity.y) ** 2 + (p.x - entity.x) ** 2 <= 9 and\
|
||||
isinstance(entity, Monster):
|
||||
self.target = entity
|
||||
entity.paths = dict() # Allows the paths to be calculated.
|
||||
break
|
||||
|
||||
# Familiars move according to a Dijkstra algorithm
|
||||
# that targets their target.
|
||||
# If they can not move and are already close to their target,
|
||||
# they hit, except if their target is the player.
|
||||
if self.target and (self.y, self.x) in self.target.paths:
|
||||
# Moves to target player by choosing the best available path
|
||||
for next_y, next_x in self.target.paths[(self.y, self.x)]:
|
||||
moved = self.check_move(next_y, next_x, True)
|
||||
if moved:
|
||||
break
|
||||
if self.distance_squared(self.target) <= 1 and \
|
||||
not isinstance(self.target, Player):
|
||||
self.map.logs.add_message(self.hit(self.target))
|
||||
break
|
||||
else:
|
||||
# Moves in a random direction
|
||||
# If the direction is not available, tries another one
|
||||
moves = [self.move_up, self.move_down,
|
||||
self.move_left, self.move_right]
|
||||
shuffle(moves)
|
||||
for move in moves:
|
||||
if move():
|
||||
break
|
||||
|
||||
|
||||
class Trumpet(Familiar):
|
||||
"""
|
||||
A class of familiars.
|
||||
"""
|
||||
def __init__(self, name: str = "trumpet", strength: int = 3,
|
||||
maxhealth: int = 30, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
@ -1,71 +1,165 @@
|
||||
from typing import Optional
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .player import Player
|
||||
from ..interfaces import Entity, FightingEntity, Map
|
||||
from random import choice, randint
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..interfaces import Entity, FightingEntity, InventoryHolder, Map
|
||||
from ..translations import gettext as _
|
||||
|
||||
|
||||
class Item(Entity):
|
||||
"""
|
||||
A class for items
|
||||
A class for items.
|
||||
"""
|
||||
held: bool
|
||||
held_by: Optional[Player]
|
||||
held_by: Optional[InventoryHolder]
|
||||
price: int
|
||||
|
||||
def __init__(self, held: bool = False, held_by: Optional[Player] = None,
|
||||
*args, **kwargs):
|
||||
def __init__(self, equipped: bool = False,
|
||||
held_by: Optional[InventoryHolder] = None,
|
||||
hold_slot: str = "equipped_secondary",
|
||||
price: int = 2, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.held = held
|
||||
self.held_by = held_by
|
||||
self.equipped = equipped
|
||||
self.hold_slot = hold_slot
|
||||
if equipped:
|
||||
self.equip()
|
||||
self.price = price
|
||||
|
||||
def drop(self, y: int, x: int) -> None:
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""
|
||||
The item is dropped from the inventory onto the floor
|
||||
In the inventory, indicate the usefulness of the item.
|
||||
"""
|
||||
if self.held:
|
||||
self.held_by.inventory.remove(self)
|
||||
self.held = False
|
||||
return ""
|
||||
|
||||
def drop(self) -> None:
|
||||
"""
|
||||
The item is dropped from the inventory onto the floor.
|
||||
"""
|
||||
if self.held_by is not None:
|
||||
self.unequip()
|
||||
self.held_by.remove_from_inventory(self)
|
||||
self.held_by.map.add_entity(self)
|
||||
self.move(self.held_by.y, self.held_by.x)
|
||||
self.held_by = None
|
||||
self.map.add_entity(self)
|
||||
self.move(y, x)
|
||||
|
||||
def hold(self, player: "Player") -> None:
|
||||
def use(self) -> None:
|
||||
"""
|
||||
The item is taken from the floor and put into the inventory
|
||||
Indicates what should be done when the item is used.
|
||||
"""
|
||||
self.held = True
|
||||
self.held_by = player
|
||||
self.map.remove_entity(self)
|
||||
player.inventory.append(self)
|
||||
|
||||
def throw(self, direction: int) -> Any:
|
||||
"""
|
||||
Indicates what should be done when the item is thrown.
|
||||
"""
|
||||
|
||||
def on_equip(self) -> None:
|
||||
"""
|
||||
Indicates a special behaviour when equipping
|
||||
"""
|
||||
|
||||
def on_unequip(self) -> None:
|
||||
"""
|
||||
Indicates a special behaviour when unequipping
|
||||
"""
|
||||
|
||||
def equip(self) -> None:
|
||||
"""
|
||||
Indicates what should be done when the item is equipped.
|
||||
"""
|
||||
# Other objects are only equipped as secondary.
|
||||
if not self.equipped:
|
||||
if getattr(self.held_by, self.hold_slot):
|
||||
getattr(self.held_by, self.hold_slot).unequip()
|
||||
self.equipped = True
|
||||
setattr(self.held_by, self.hold_slot, self)
|
||||
self.on_equip()
|
||||
|
||||
def unequip(self) -> None:
|
||||
"""
|
||||
Indicates what should be done when the item is unequipped.
|
||||
"""
|
||||
if self.equipped:
|
||||
setattr(self.held_by, self.hold_slot, None)
|
||||
self.equipped = False
|
||||
self.on_unequip()
|
||||
|
||||
def hold(self, holder: InventoryHolder) -> None:
|
||||
"""
|
||||
The item is taken from the floor and put into the inventory.
|
||||
"""
|
||||
self.held_by = holder
|
||||
self.held_by.map.remove_entity(self)
|
||||
holder.add_to_inventory(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
Saves the state of the item into a dictionary.
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["held"] = self.held
|
||||
d["equipped"] = self.equipped
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def get_all_items() -> list:
|
||||
"""
|
||||
Returns the list of all item classes.
|
||||
"""
|
||||
return [BodySnatchPotion, Bomb, Bow, Chestplate, FireBallStaff,
|
||||
Heart, Helmet, Monocle, ScrollofDamage, ScrollofWeakening,
|
||||
Shield, Sword, RingCritical, RingXP, Ruler]
|
||||
|
||||
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder,
|
||||
for_free: bool = False) -> bool:
|
||||
"""
|
||||
Does all necessary actions when an object is to be sold.
|
||||
Is overwritten by some classes that cannot exist in the player's
|
||||
inventory.
|
||||
"""
|
||||
if for_free:
|
||||
self.unequip() if self.equipped else None
|
||||
self.hold(buyer)
|
||||
seller.remove_from_inventory(self)
|
||||
return True
|
||||
elif buyer.hazel >= self.price:
|
||||
self.unequip() if self.equipped else None
|
||||
self.hold(buyer)
|
||||
seller.remove_from_inventory(self)
|
||||
buyer.change_hazel_balance(-self.price)
|
||||
seller.change_hazel_balance(self.price)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Heart(Item):
|
||||
"""
|
||||
A heart item to return health to the player
|
||||
A heart item to return health to the player.
|
||||
"""
|
||||
healing: int
|
||||
|
||||
def __init__(self, healing: int = 5, *args, **kwargs):
|
||||
super().__init__(name="heart", *args, **kwargs)
|
||||
def __init__(self, name: str = "heart", healing: int = 5, price: int = 3,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
self.healing = healing
|
||||
|
||||
def hold(self, player: "Player") -> None:
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return f"HP+{self.healing}"
|
||||
|
||||
def hold(self, entity: InventoryHolder) -> None:
|
||||
"""
|
||||
When holding a heart, heal the player and don't put item in inventory.
|
||||
When holding a heart, the player is healed and
|
||||
the item is not put in the inventory.
|
||||
"""
|
||||
player.health = min(player.maxhealth, player.health + self.healing)
|
||||
self.map.remove_entity(self)
|
||||
entity.health = min(entity.maxhealth, entity.health + self.healing)
|
||||
entity.map.remove_entity(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the header into a dictionary
|
||||
Saves the state of the heart into a dictionary.
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["healing"] = self.healing
|
||||
@ -78,32 +172,467 @@ class Bomb(Item):
|
||||
"""
|
||||
damage: int = 5
|
||||
exploding: bool
|
||||
owner: Optional["InventoryHolder"]
|
||||
tick: int
|
||||
|
||||
def __init__(self, damage: int = 5, exploding: bool = False,
|
||||
*args, **kwargs):
|
||||
super().__init__(name="bomb", *args, **kwargs)
|
||||
def __init__(self, name: str = "bomb", damage: int = 5,
|
||||
exploding: bool = False, price: int = 4, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
self.damage = damage
|
||||
self.exploding = exploding
|
||||
self.tick = 4
|
||||
self.owner = None
|
||||
|
||||
def drop(self, x: int, y: int) -> None:
|
||||
super().drop(x, y)
|
||||
self.exploding = True
|
||||
def use(self) -> None:
|
||||
"""
|
||||
When the bomb is used, it is thrown and then it explodes.
|
||||
"""
|
||||
if self.held_by is not None:
|
||||
self.owner = self.held_by
|
||||
super().drop()
|
||||
self.exploding = True
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
Special exploding action of the bomb
|
||||
Special exploding action of the bomb.
|
||||
"""
|
||||
if self.exploding:
|
||||
for e in m.entities.copy():
|
||||
if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \
|
||||
isinstance(e, FightingEntity):
|
||||
e.take_damage(self, self.damage)
|
||||
if self.tick > 0:
|
||||
# The bomb will explode in <tick> moves
|
||||
self.tick -= 1
|
||||
else:
|
||||
# The bomb is exploding.
|
||||
# Each entity that is close to the bomb takes damages.
|
||||
# The player earn XP if the entity was killed.
|
||||
log_message = _("Bomb is exploding.")
|
||||
for e in m.entities.copy():
|
||||
if abs(e.x - self.x) + abs(e.y - self.y) <= 3 and \
|
||||
isinstance(e, FightingEntity):
|
||||
log_message += " " + e.take_damage(self, self.damage)
|
||||
if e.dead:
|
||||
self.owner.add_xp(randint(3, 7))
|
||||
m.logs.add_message(log_message)
|
||||
m.entities.remove(self)
|
||||
|
||||
# Add sparkles where the bomb exploded.
|
||||
explosion = Explosion(y=self.y, x=self.x)
|
||||
self.map.add_entity(explosion)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the bomb into a dictionary
|
||||
Saves the state of the bomb into a dictionary.
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["exploding"] = self.exploding
|
||||
d["damage"] = self.damage
|
||||
return d
|
||||
|
||||
|
||||
class Explosion(Item):
|
||||
"""
|
||||
When a bomb explodes, the explosion is displayed.
|
||||
"""
|
||||
living_ticks: int
|
||||
|
||||
def __init__(self, living_ticks: int = 2, *args, **kwargs):
|
||||
super().__init__(name="explosion", *args, **kwargs)
|
||||
self.living_ticks = living_ticks
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
The bomb disappears after exploding.
|
||||
"""
|
||||
self.living_ticks -= 1
|
||||
if self.living_ticks <= 0:
|
||||
m.remove_entity(self)
|
||||
|
||||
def hold(self, player: InventoryHolder) -> None:
|
||||
"""
|
||||
The player can't hold an explosion.
|
||||
"""
|
||||
|
||||
|
||||
class Weapon(Item):
|
||||
"""
|
||||
Non-throwable items that improve player damage
|
||||
"""
|
||||
damage: int
|
||||
|
||||
def __init__(self, damage: int = 3, *args, **kwargs):
|
||||
super().__init__(hold_slot="equipped_main", *args, **kwargs)
|
||||
self.damage = damage
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return f"STR+{self.damage}" if self.damage else super().description
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the weapon into a dictionary
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["damage"] = self.damage
|
||||
return d
|
||||
|
||||
def on_equip(self) -> None:
|
||||
"""
|
||||
When a weapon is equipped, the player gains strength.
|
||||
"""
|
||||
self.held_by.strength += self.damage
|
||||
|
||||
def on_unequip(self) -> None:
|
||||
"""
|
||||
Remove the strength earned by the weapon.
|
||||
:return:
|
||||
"""
|
||||
self.held_by.strength -= self.damage
|
||||
|
||||
|
||||
class Sword(Weapon):
|
||||
"""
|
||||
A basic weapon
|
||||
"""
|
||||
def __init__(self, name: str = "sword", price: int = 20,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
|
||||
|
||||
class Ruler(Weapon):
|
||||
"""
|
||||
A basic weapon
|
||||
"""
|
||||
def __init__(self, name: str = "ruler", price: int = 2,
|
||||
damage: int = 1, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, damage=damage, *args, **kwargs)
|
||||
|
||||
|
||||
class Armor(Item):
|
||||
"""
|
||||
Class of items that increase the player's constitution.
|
||||
"""
|
||||
constitution: int
|
||||
|
||||
def __init__(self, constitution: int, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.constitution = constitution
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return f"CON+{self.constitution}" if self.constitution \
|
||||
else super().description
|
||||
|
||||
def on_equip(self) -> None:
|
||||
self.held_by.constitution += self.constitution
|
||||
|
||||
def on_unequip(self) -> None:
|
||||
self.held_by.constitution -= self.constitution
|
||||
|
||||
def save_state(self) -> dict:
|
||||
d = super().save_state()
|
||||
d["constitution"] = self.constitution
|
||||
return d
|
||||
|
||||
|
||||
class Shield(Armor):
|
||||
"""
|
||||
Class of shield items, they can be equipped in the other hand.
|
||||
"""
|
||||
def __init__(self, name: str = "shield", constitution: int = 2,
|
||||
price: int = 16, *args, **kwargs):
|
||||
super().__init__(name=name, constitution=constitution, price=price,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class Helmet(Armor):
|
||||
"""
|
||||
Class of helmet items, they can be equipped on the head.
|
||||
"""
|
||||
def __init__(self, name: str = "helmet", constitution: int = 2,
|
||||
price: int = 18, *args, **kwargs):
|
||||
super().__init__(name=name, constitution=constitution, price=price,
|
||||
hold_slot="equipped_helmet", *args, **kwargs)
|
||||
|
||||
|
||||
class Chestplate(Armor):
|
||||
"""
|
||||
Class of chestplate items, they can be equipped on the body.
|
||||
"""
|
||||
def __init__(self, name: str = "chestplate", constitution: int = 4,
|
||||
price: int = 30, *args, **kwargs):
|
||||
super().__init__(name=name, constitution=constitution, price=price,
|
||||
hold_slot="equipped_armor", *args, **kwargs)
|
||||
|
||||
|
||||
class BodySnatchPotion(Item):
|
||||
"""
|
||||
The body-snatch potion allows to exchange all characteristics with a random
|
||||
other entity.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "body_snatch_potion", price: int = 14,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
|
||||
def use(self) -> None:
|
||||
"""
|
||||
Find a valid random entity, then exchange characteristics.
|
||||
"""
|
||||
valid_entities = self.held_by.map.find_entities(FightingEntity)
|
||||
valid_entities.remove(self.held_by)
|
||||
entity = choice(valid_entities)
|
||||
entity_state = entity.save_state()
|
||||
player_state = self.held_by.save_state()
|
||||
self.held_by.__dict__.update(entity_state)
|
||||
entity.__dict__.update(player_state)
|
||||
self.held_by.map.currenty, self.held_by.map.currentx = self.held_by.y,\
|
||||
self.held_by.x
|
||||
|
||||
self.held_by.map.logs.add_message(
|
||||
_("{player} exchanged its body with {entity}.").format(
|
||||
player=self.held_by.translated_name.capitalize(),
|
||||
entity=entity.translated_name))
|
||||
|
||||
self.held_by.recalculate_paths()
|
||||
|
||||
self.held_by.inventory.remove(self)
|
||||
|
||||
|
||||
class Ring(Item):
|
||||
"""
|
||||
A class of rings that boost the player's statistics.
|
||||
"""
|
||||
maxhealth: int
|
||||
strength: int
|
||||
intelligence: int
|
||||
charisma: int
|
||||
dexterity: int
|
||||
constitution: int
|
||||
critical: int
|
||||
experience: float
|
||||
|
||||
def __init__(self, maxhealth: int = 0, strength: int = 0,
|
||||
intelligence: int = 0, charisma: int = 0,
|
||||
dexterity: int = 0, constitution: int = 0,
|
||||
critical: int = 0, experience: float = 0, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.maxhealth = maxhealth
|
||||
self.strength = strength
|
||||
self.intelligence = intelligence
|
||||
self.charisma = charisma
|
||||
self.dexterity = dexterity
|
||||
self.constitution = constitution
|
||||
self.critical = critical
|
||||
self.experience = experience
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
fields = [("MAX HP", self.maxhealth), ("STR", self.strength),
|
||||
("INT", self.intelligence), ("CHR", self.charisma),
|
||||
("DEX", self.dexterity), ("CON", self.constitution),
|
||||
("CRI", self.critical), ("XP", self.experience)]
|
||||
return ", ".join(f"{key}+{value}" for key, value in fields if value)
|
||||
|
||||
def on_equip(self) -> None:
|
||||
self.held_by.maxhealth += self.maxhealth
|
||||
self.held_by.strength += self.strength
|
||||
self.held_by.intelligence += self.intelligence
|
||||
self.held_by.charisma += self.charisma
|
||||
self.held_by.dexterity += self.dexterity
|
||||
self.held_by.constitution += self.constitution
|
||||
self.held_by.critical += self.critical
|
||||
self.held_by.xp_buff += self.experience
|
||||
|
||||
def on_unequip(self) -> None:
|
||||
self.held_by.maxhealth -= self.maxhealth
|
||||
self.held_by.strength -= self.strength
|
||||
self.held_by.intelligence -= self.intelligence
|
||||
self.held_by.charisma -= self.charisma
|
||||
self.held_by.dexterity -= self.dexterity
|
||||
self.held_by.constitution -= self.constitution
|
||||
self.held_by.critical -= self.critical
|
||||
self.held_by.xp_buff -= self.experience
|
||||
|
||||
def save_state(self) -> dict:
|
||||
d = super().save_state()
|
||||
d["maxhealth"] = self.maxhealth
|
||||
d["strength"] = self.strength
|
||||
d["intelligence"] = self.intelligence
|
||||
d["charisma"] = self.charisma
|
||||
d["dexterity"] = self.dexterity
|
||||
d["constitution"] = self.constitution
|
||||
d["critical"] = self.critical
|
||||
d["experience"] = self.experience
|
||||
return d
|
||||
|
||||
|
||||
class RingCritical(Ring):
|
||||
def __init__(self, name: str = "ring_of_critical_damage", price: int = 15,
|
||||
critical: int = 20, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, critical=critical,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class RingXP(Ring):
|
||||
def __init__(self, name: str = "ring_of_more_experience", price: int = 25,
|
||||
experience: float = 2, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, experience=experience,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class ScrollofDamage(Item):
|
||||
"""
|
||||
A scroll that, when used, deals damage to all entities in a certain radius.
|
||||
"""
|
||||
def __init__(self, name: str = "scroll_of_damage", price: int = 18,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
|
||||
def use(self) -> None:
|
||||
"""
|
||||
Find all entities within a radius of 5, and deal damage based on the
|
||||
player's intelligence.
|
||||
"""
|
||||
for entity in self.held_by.map.entities:
|
||||
if entity.is_fighting_entity() and not entity == self.held_by:
|
||||
if entity.distance(self.held_by) <= 5:
|
||||
self.held_by.map.logs.add_message(entity.take_damage(
|
||||
self.held_by, self.held_by.intelligence))
|
||||
self.held_by.inventory.remove(self)
|
||||
|
||||
|
||||
class ScrollofWeakening(Item):
|
||||
"""
|
||||
A scroll that, when used, reduces the damage of the ennemies for 3 turns.
|
||||
"""
|
||||
def __init__(self, name: str = "scroll_of_weakening", price: int = 13,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
|
||||
def use(self) -> None:
|
||||
"""
|
||||
Find all entities and reduce their damage.
|
||||
"""
|
||||
for entity in self.held_by.map.entities:
|
||||
if entity.is_fighting_entity() and not entity == self.held_by:
|
||||
entity.strength = entity.strength - \
|
||||
max(1, self.held_by.intelligence // 2)
|
||||
entity.effects.append(["strength",
|
||||
-max(1, self.held_by.intelligence // 2),
|
||||
3])
|
||||
self.held_by.map.logs.add_message(
|
||||
_(f"The ennemies have -{max(1, self.held_by.intelligence // 2)}"
|
||||
+ "strength for 3 turns"))
|
||||
self.held_by.inventory.remove(self)
|
||||
|
||||
|
||||
class LongRangeWeapon(Weapon):
|
||||
def __init__(self, damage: int = 4,
|
||||
rang: int = 3, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.damage = damage
|
||||
self.range = rang
|
||||
|
||||
def throw(self, direction: int) -> Any:
|
||||
to_kill = None
|
||||
for entity in self.held_by.map.entities:
|
||||
if entity.is_fighting_entity():
|
||||
if direction == 0 and self.held_by.x == entity.x \
|
||||
and self.held_by.y - entity.y > 0 and \
|
||||
self.held_by.y - entity.y <= self.range:
|
||||
to_kill = entity
|
||||
elif direction == 2 and self.held_by.x == entity.x \
|
||||
and entity.y - self.held_by.y > 0 and \
|
||||
entity.y - self.held_by.y <= self.range:
|
||||
to_kill = entity
|
||||
elif direction == 1 and self.held_by.y == entity.y \
|
||||
and entity.x - self.held_by.x > 0 and \
|
||||
entity.x - self.held_by.x <= self.range:
|
||||
to_kill = entity
|
||||
elif direction == 3 and self.held_by.y == entity.y \
|
||||
and self.held_by.x - entity.x > 0 and \
|
||||
self.held_by.x - entity.x <= self.range:
|
||||
to_kill = entity
|
||||
if to_kill:
|
||||
line = _("{name}").format(name=to_kill.translated_name.capitalize()
|
||||
) + self.string + " "\
|
||||
+ to_kill.take_damage(
|
||||
self.held_by, self.damage
|
||||
+ getattr(self.held_by, self.stat))
|
||||
self.held_by.map.logs.add_message(line)
|
||||
return (to_kill.y, to_kill.x) if to_kill else None
|
||||
|
||||
@property
|
||||
def stat(self) -> str:
|
||||
"""
|
||||
The stat that is used when using the object: dexterity for a bow
|
||||
or intelligence for a magic staff.
|
||||
"""
|
||||
|
||||
@property
|
||||
def string(self) -> str:
|
||||
"""
|
||||
The string that is printed when we hit an ennemy.
|
||||
"""
|
||||
|
||||
|
||||
class Bow(LongRangeWeapon):
|
||||
"""
|
||||
A type of long range weapon that deals damage
|
||||
based on the player's dexterity
|
||||
"""
|
||||
def __init__(self, name: str = "bow", price: int = 22, damage: int = 4,
|
||||
rang: int = 3, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, damage=damage,
|
||||
rang=rang, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def stat(self) -> str:
|
||||
"""
|
||||
Here it is dexterity
|
||||
"""
|
||||
return "dexterity"
|
||||
|
||||
@property
|
||||
def string(self) -> str:
|
||||
return _(" is shot by an arrow.")
|
||||
|
||||
|
||||
class FireBallStaff(LongRangeWeapon):
|
||||
"""
|
||||
A type of powerful long range weapon that deals damage
|
||||
based on the player's intelligence
|
||||
"""
|
||||
def __init__(self, name: str = "fire_ball_staff", price: int = 36,
|
||||
damage: int = 6, rang: int = 4, *args, **kwargs):
|
||||
super().__init__(name=name, price=price, damage=damage,
|
||||
rang=rang, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def stat(self) -> str:
|
||||
"""
|
||||
Here it is intelligence
|
||||
"""
|
||||
return "intelligence"
|
||||
|
||||
@property
|
||||
def string(self) -> str:
|
||||
return _(" is shot by a fire ball.")
|
||||
|
||||
def throw(self, direction: int) -> Any:
|
||||
"""
|
||||
Adds an explosion animation when killing something.
|
||||
"""
|
||||
coord = super().throw(direction)
|
||||
if coord:
|
||||
y = coord[0]
|
||||
x = coord[1]
|
||||
|
||||
explosion = Explosion(y=y, x=x)
|
||||
self.held_by.map.add_entity(explosion)
|
||||
return y, x
|
||||
|
||||
|
||||
class Monocle(Item):
|
||||
def __init__(self, name: str = "monocle", price: int = 10,
|
||||
*args, **kwargs):
|
||||
super().__init__(name=name, price=price, *args, **kwargs)
|
||||
|
@ -1,4 +1,7 @@
|
||||
from random import choice
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import shuffle
|
||||
|
||||
from .player import Player
|
||||
from ..interfaces import FightingEntity, Map
|
||||
@ -7,8 +10,8 @@ from ..interfaces import FightingEntity, Map
|
||||
class Monster(FightingEntity):
|
||||
"""
|
||||
The class for all monsters in the dungeon.
|
||||
A monster must override this class, and the parameters are given
|
||||
in the __init__ function.
|
||||
All specific monster classes overwrite this class,
|
||||
and the parameters are given in the __init__ function.
|
||||
An example of the specification of a monster that has a strength of 4
|
||||
and 20 max HP:
|
||||
|
||||
@ -18,7 +21,7 @@ class Monster(FightingEntity):
|
||||
super().__init__(name="my_monster", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
With that way, attributes can be overwritten when the entity got created.
|
||||
With that way, attributes can be overwritten when the entity is created.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -26,8 +29,9 @@ class Monster(FightingEntity):
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
By default, a monster will move randomly where it is possible
|
||||
And if a player is close to the monster, the monster run on the player.
|
||||
If the player is closeby, the monster runs to the player.
|
||||
"""
|
||||
super().act(m)
|
||||
target = None
|
||||
for entity in m.entities:
|
||||
if self.distance_squared(entity) <= 25 and \
|
||||
@ -35,58 +39,86 @@ class Monster(FightingEntity):
|
||||
target = entity
|
||||
break
|
||||
|
||||
# A Dijkstra algorithm has ran that targets the player.
|
||||
# With that way, monsters can simply follow the path.
|
||||
# If they can't move and they are already close to the player,
|
||||
# They hit.
|
||||
if target and (self.y, self.x) in target.paths:
|
||||
# Move to target player
|
||||
next_y, next_x = target.paths[(self.y, self.x)]
|
||||
moved = self.check_move(next_y, next_x, True)
|
||||
if not moved and self.distance_squared(target) <= 1:
|
||||
self.map.logs.add_message(self.hit(target))
|
||||
# Monsters move according to a Dijkstra algorithm
|
||||
# that targets the player.
|
||||
# If they can not move and are already close to the player,
|
||||
# they hit.
|
||||
if target and (self.y, self.x) in target.paths and \
|
||||
self.map.is_visible_from(self.y, self.x,
|
||||
target.y, target.x, 5):
|
||||
# Moves to target player by choosing the best available path
|
||||
for next_y, next_x in target.paths[(self.y, self.x)]:
|
||||
moved = self.check_move(next_y, next_x, True)
|
||||
if moved:
|
||||
break
|
||||
if self.distance_squared(target) <= 1:
|
||||
self.map.logs.add_message(self.hit(target))
|
||||
break
|
||||
else:
|
||||
for _ in range(100):
|
||||
if choice([self.move_up, self.move_down,
|
||||
self.move_left, self.move_right])():
|
||||
# Moves in a random direction
|
||||
# If the direction is not available, tries another one
|
||||
moves = [self.move_up, self.move_down,
|
||||
self.move_left, self.move_right]
|
||||
shuffle(moves)
|
||||
for move in moves:
|
||||
if move():
|
||||
break
|
||||
|
||||
def move(self, y: int, x: int) -> None:
|
||||
"""
|
||||
Overwrites the move function to recalculate paths.
|
||||
"""
|
||||
super().move(y, x)
|
||||
self.recalculate_paths()
|
||||
|
||||
class Beaver(Monster):
|
||||
|
||||
class Tiger(Monster):
|
||||
"""
|
||||
A beaver monster
|
||||
A tiger monster.
|
||||
"""
|
||||
def __init__(self, strength: int = 2, maxhealth: int = 20,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="beaver", strength=strength,
|
||||
def __init__(self, name: str = "tiger", strength: int = 5,
|
||||
maxhealth: int = 30, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class Hedgehog(Monster):
|
||||
"""
|
||||
A really mean hedgehog monster
|
||||
A really mean hedgehog monster.
|
||||
"""
|
||||
def __init__(self, strength: int = 3, maxhealth: int = 10,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="hedgehog", strength=strength,
|
||||
def __init__(self, name: str = "hedgehog", strength: int = 3,
|
||||
maxhealth: int = 10, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class Rabbit(Monster):
|
||||
"""
|
||||
A rabbit monster
|
||||
A rabbit monster.
|
||||
"""
|
||||
def __init__(self, strength: int = 1, maxhealth: int = 15,
|
||||
def __init__(self, name: str = "rabbit", strength: int = 1,
|
||||
maxhealth: int = 20, critical: int = 30,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="rabbit", strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, critical=critical,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class TeddyBear(Monster):
|
||||
"""
|
||||
A cute teddybear monster
|
||||
A cute teddybear monster.
|
||||
"""
|
||||
def __init__(self, strength: int = 0, maxhealth: int = 50,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(name="teddy_bear", strength=strength,
|
||||
def __init__(self, name: str = "teddy_bear", strength: int = 0,
|
||||
maxhealth: int = 50, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
||||
|
||||
class GiantSeaEagle(Monster):
|
||||
"""
|
||||
An eagle boss
|
||||
"""
|
||||
def __init__(self, name: str = "eagle", strength: int = 1000,
|
||||
maxhealth: int = 5000, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, strength=strength,
|
||||
maxhealth=maxhealth, *args, **kwargs)
|
||||
|
@ -1,30 +1,56 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from math import log
|
||||
from random import randint
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from ..interfaces import FightingEntity
|
||||
from .items import Item
|
||||
from ..interfaces import FightingEntity, InventoryHolder, Tile
|
||||
from ..translations import gettext as _
|
||||
|
||||
|
||||
class Player(FightingEntity):
|
||||
class Player(InventoryHolder, FightingEntity):
|
||||
"""
|
||||
The class of the player
|
||||
The class of the player.
|
||||
"""
|
||||
current_xp: int = 0
|
||||
max_xp: int = 10
|
||||
inventory: list
|
||||
xp_buff: float = 1
|
||||
paths: Dict[Tuple[int, int], Tuple[int, int]]
|
||||
equipped_main: Optional[Item]
|
||||
equipped_secondary: Optional[Item]
|
||||
equipped_helmet: Optional[Item]
|
||||
equipped_armor: Optional[Item]
|
||||
|
||||
def __init__(self, maxhealth: int = 20, strength: int = 5,
|
||||
intelligence: int = 1, charisma: int = 1, dexterity: int = 1,
|
||||
constitution: int = 1, level: int = 1, current_xp: int = 0,
|
||||
max_xp: int = 10, *args, **kwargs) -> None:
|
||||
super().__init__(name="player", maxhealth=maxhealth, strength=strength,
|
||||
def __init__(self, name: str = "player", maxhealth: int = 20,
|
||||
strength: int = 5, intelligence: int = 1, charisma: int = 1,
|
||||
dexterity: int = 1, constitution: int = 1, level: int = 1,
|
||||
current_xp: int = 0, max_xp: int = 10, inventory: list = None,
|
||||
hazel: int = 42, equipped_main: Optional[Item] = None,
|
||||
equipped_armor: Optional[Item] = None, critical: int = 5,
|
||||
equipped_secondary: Optional[Item] = None,
|
||||
equipped_helmet: Optional[Item] = None, xp_buff: float = 1,
|
||||
vision: int = 5, *args, **kwargs) -> None:
|
||||
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
|
||||
intelligence=intelligence, charisma=charisma,
|
||||
dexterity=dexterity, constitution=constitution,
|
||||
level=level, *args, **kwargs)
|
||||
level=level, critical=critical, *args, **kwargs)
|
||||
self.current_xp = current_xp
|
||||
self.max_xp = max_xp
|
||||
self.inventory = list()
|
||||
self.xp_buff = xp_buff
|
||||
self.inventory = self.translate_inventory(inventory or [])
|
||||
self.paths = dict()
|
||||
self.hazel = hazel
|
||||
self.equipped_main = self.dict_to_item(equipped_main) \
|
||||
if isinstance(equipped_main, dict) else equipped_main
|
||||
self.equipped_armor = self.dict_to_item(equipped_armor) \
|
||||
if isinstance(equipped_armor, dict) else equipped_armor
|
||||
self.equipped_secondary = self.dict_to_item(equipped_secondary) \
|
||||
if isinstance(equipped_secondary, dict) else equipped_secondary
|
||||
self.equipped_helmet = self.dict_to_item(equipped_helmet) \
|
||||
if isinstance(equipped_helmet, dict) else equipped_helmet
|
||||
self.vision = vision
|
||||
|
||||
def move(self, y: int, x: int) -> None:
|
||||
"""
|
||||
@ -35,27 +61,60 @@ class Player(FightingEntity):
|
||||
self.map.currenty = y
|
||||
self.map.currentx = x
|
||||
self.recalculate_paths()
|
||||
self.map.compute_visibility(self.y, self.x, self.vision)
|
||||
|
||||
def dance(self) -> None:
|
||||
"""
|
||||
Dancing has a certain probability or making ennemies unable
|
||||
to fight for 3 turns. That probability depends on the player's
|
||||
charisma.
|
||||
"""
|
||||
diceroll = randint(1, 10)
|
||||
found = False
|
||||
if diceroll <= self.charisma:
|
||||
for entity in self.map.entities:
|
||||
if entity.is_fighting_entity() and not entity == self \
|
||||
and entity.distance(self) <= 3:
|
||||
found = True
|
||||
entity.confused = 1
|
||||
entity.effects.append(["confused", 1, 3])
|
||||
if found:
|
||||
self.map.logs.add_message(_(
|
||||
"It worked! Nearby ennemies will be confused for 3 turns."))
|
||||
else:
|
||||
self.map.logs.add_message(_(
|
||||
"It worked, but there is no one nearby..."))
|
||||
else:
|
||||
self.map.logs.add_message(
|
||||
_("The dance was not effective..."))
|
||||
|
||||
def level_up(self) -> None:
|
||||
"""
|
||||
Add levels to the player as much as it is possible.
|
||||
Add as many levels as possible to the player.
|
||||
"""
|
||||
while self.current_xp > self.max_xp:
|
||||
self.level += 1
|
||||
self.current_xp -= self.max_xp
|
||||
self.max_xp = self.level * 10
|
||||
self.maxhealth += int(2 * log(self.level) / log(2))
|
||||
self.health = self.maxhealth
|
||||
self.strength = self.strength + 1
|
||||
# TODO Remove it, that's only fun
|
||||
self.map.spawn_random_entities(randint(3 * self.level,
|
||||
10 * self.level))
|
||||
if self.level % 3 == 0:
|
||||
self.dexterity += 1
|
||||
self.constitution += 1
|
||||
if self.level % 4 == 0:
|
||||
self.intelligence += 1
|
||||
if self.level % 6 == 0:
|
||||
self.charisma += 1
|
||||
if self.level % 10 == 0 and self.critical < 95:
|
||||
self.critical += (100 - self.charisma) // 30
|
||||
|
||||
def add_xp(self, xp: int) -> None:
|
||||
"""
|
||||
Add some experience to the player.
|
||||
If the required amount is reached, level up.
|
||||
Adds some experience to the player.
|
||||
If the required amount is reached, the player levels up.
|
||||
"""
|
||||
self.current_xp += xp
|
||||
self.current_xp += int(xp * self.xp_buff)
|
||||
self.level_up()
|
||||
|
||||
# noinspection PyTypeChecker,PyUnresolvedReferences
|
||||
@ -78,35 +137,14 @@ class Player(FightingEntity):
|
||||
return True
|
||||
elif entity.is_item():
|
||||
entity.hold(self)
|
||||
tile = self.map.tiles[y][x]
|
||||
if tile == Tile.DOOR and move_if_possible:
|
||||
# Open door
|
||||
self.map.tiles[y][x] = Tile.FLOOR
|
||||
self.map.compute_visibility(y, x, self.vision)
|
||||
return super().check_move(y, x, move_if_possible)
|
||||
return super().check_move(y, x, move_if_possible)
|
||||
|
||||
def recalculate_paths(self, max_distance: int = 8) -> None:
|
||||
"""
|
||||
Use Dijkstra algorithm to calculate best paths
|
||||
for monsters to go to the player.
|
||||
"""
|
||||
queue = [(self.y, self.x)]
|
||||
visited = []
|
||||
distances = {(self.y, self.x): 0}
|
||||
predecessors = {}
|
||||
while queue:
|
||||
y, x = queue.pop(0)
|
||||
visited.append((y, x))
|
||||
if distances[(y, x)] >= max_distance:
|
||||
continue
|
||||
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
||||
new_y, new_x = y + diff_y, x + diff_x
|
||||
if not 0 <= new_y < self.map.height or \
|
||||
not 0 <= new_x < self.map.width or \
|
||||
not self.map.tiles[y][x].can_walk() or \
|
||||
(new_y, new_x) in visited or \
|
||||
(new_y, new_x) in queue:
|
||||
continue
|
||||
predecessors[(new_y, new_x)] = (y, x)
|
||||
distances[(new_y, new_x)] = distances[(y, x)] + 1
|
||||
queue.append((new_y, new_x))
|
||||
self.paths = predecessors
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
@ -114,4 +152,12 @@ class Player(FightingEntity):
|
||||
d = super().save_state()
|
||||
d["current_xp"] = self.current_xp
|
||||
d["max_xp"] = self.max_xp
|
||||
d["equipped_main"] = self.equipped_main.save_state()\
|
||||
if self.equipped_main else None
|
||||
d["equipped_armor"] = self.equipped_armor.save_state()\
|
||||
if self.equipped_armor else None
|
||||
d["equipped_secondary"] = self.equipped_secondary.save_state()\
|
||||
if self.equipped_secondary else None
|
||||
d["equipped_helmet"] = self.equipped_helmet.save_state()\
|
||||
if self.equipped_helmet else None
|
||||
return d
|
||||
|
@ -1,4 +1,7 @@
|
||||
from enum import Enum, auto
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from enum import auto, Enum
|
||||
from typing import Optional
|
||||
|
||||
from squirrelbattle.settings import Settings
|
||||
@ -13,33 +16,47 @@ class DisplayActions(Enum):
|
||||
"""
|
||||
REFRESH = auto()
|
||||
UPDATE = auto()
|
||||
MOUSE = auto()
|
||||
|
||||
|
||||
class GameMode(Enum):
|
||||
"""
|
||||
Game mode options
|
||||
Game mode options.
|
||||
"""
|
||||
MAINMENU = auto()
|
||||
PLAY = auto()
|
||||
SETTINGS = auto()
|
||||
INVENTORY = auto()
|
||||
STORE = auto()
|
||||
CHEST = auto()
|
||||
CREDITS = auto()
|
||||
|
||||
|
||||
class KeyValues(Enum):
|
||||
"""
|
||||
Key values options used in the game
|
||||
Key values options used in the game.
|
||||
"""
|
||||
UP = auto()
|
||||
DOWN = auto()
|
||||
LEFT = auto()
|
||||
RIGHT = auto()
|
||||
ENTER = auto()
|
||||
INVENTORY = auto()
|
||||
USE = auto()
|
||||
EQUIP = auto()
|
||||
DROP = auto()
|
||||
SPACE = auto()
|
||||
CHAT = auto()
|
||||
WAIT = auto()
|
||||
LADDER = auto()
|
||||
LAUNCH = auto()
|
||||
DANCE = auto()
|
||||
|
||||
@staticmethod
|
||||
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
|
||||
def translate_key(key: str, settings: Settings) \
|
||||
-> Optional["KeyValues"]: # noqa: C901
|
||||
"""
|
||||
Translate the raw string key into an enum value that we can use.
|
||||
Translates the raw string key into an enum value that we can use.
|
||||
"""
|
||||
if key in (settings.KEY_DOWN_SECONDARY,
|
||||
settings.KEY_DOWN_PRIMARY):
|
||||
@ -55,6 +72,23 @@ class KeyValues(Enum):
|
||||
return KeyValues.UP
|
||||
elif key == settings.KEY_ENTER:
|
||||
return KeyValues.ENTER
|
||||
elif key == settings.KEY_INVENTORY:
|
||||
return KeyValues.INVENTORY
|
||||
elif key == settings.KEY_USE:
|
||||
return KeyValues.USE
|
||||
elif key == settings.KEY_EQUIP:
|
||||
return KeyValues.EQUIP
|
||||
elif key == settings.KEY_DROP:
|
||||
return KeyValues.DROP
|
||||
elif key == ' ':
|
||||
return KeyValues.SPACE
|
||||
return None
|
||||
elif key == settings.KEY_CHAT:
|
||||
return KeyValues.CHAT
|
||||
elif key == settings.KEY_WAIT:
|
||||
return KeyValues.WAIT
|
||||
elif key == settings.KEY_LADDER:
|
||||
return KeyValues.LADDER
|
||||
elif key == settings.KEY_LAUNCH:
|
||||
return KeyValues.LAUNCH
|
||||
elif key == settings.KEY_DANCE:
|
||||
return KeyValues.DANCE
|
||||
|
@ -1,103 +1,387 @@
|
||||
from random import randint
|
||||
from typing import Any, Optional
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from . import menus
|
||||
from .entities.player import Player
|
||||
from .enums import GameMode, KeyValues, DisplayActions
|
||||
from .interfaces import Map, Logs
|
||||
from .enums import DisplayActions, GameMode, KeyValues
|
||||
from .interfaces import Logs, Map
|
||||
from .mapgeneration import broguelike
|
||||
from .resources import ResourceManager
|
||||
from .settings import Settings
|
||||
from . import menus
|
||||
from typing import Callable
|
||||
from .translations import gettext as _, Translator
|
||||
|
||||
|
||||
class Game:
|
||||
"""
|
||||
The game object controls all actions in the game.
|
||||
"""
|
||||
map: Map
|
||||
maps: List[Map]
|
||||
map_index: int
|
||||
player: Player
|
||||
screen: Any
|
||||
# display_actions is a display interface set by the bootstrapper
|
||||
display_actions: Callable[[DisplayActions], None]
|
||||
display_actions: callable
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Init the game.
|
||||
Initiates the game.
|
||||
"""
|
||||
self.state = GameMode.MAINMENU
|
||||
self.main_menu = menus.MainMenu()
|
||||
self.settings_menu = menus.SettingsMenu()
|
||||
self.waiting_for_friendly_key = False
|
||||
self.waiting_for_launch_key = False
|
||||
self.is_in_store_menu = True
|
||||
self.is_in_chest_menu = True
|
||||
self.settings = Settings()
|
||||
self.settings.load_settings()
|
||||
self.settings.write_settings()
|
||||
Translator.setlocale(self.settings.LOCALE)
|
||||
self.main_menu = menus.MainMenu()
|
||||
self.settings_menu = menus.SettingsMenu()
|
||||
self.settings_menu.update_values(self.settings)
|
||||
self.inventory_menu = menus.InventoryMenu()
|
||||
self.store_menu = menus.StoreMenu()
|
||||
self.chest_menu = menus.ChestMenu()
|
||||
self.logs = Logs()
|
||||
self.message = None
|
||||
|
||||
def new_game(self) -> None:
|
||||
"""
|
||||
Create a new game on the screen.
|
||||
Creates a new game on the screen.
|
||||
"""
|
||||
# TODO generate a new map procedurally
|
||||
self.map = Map.load(ResourceManager.get_asset_path("example_map_2.txt"))
|
||||
self.maps = []
|
||||
self.map_index = 0
|
||||
self.map = broguelike.Generator().run()
|
||||
self.map.logs = self.logs
|
||||
self.logs.clear()
|
||||
self.player = Player()
|
||||
self.map.add_entity(self.player)
|
||||
self.player.move(self.map.start_y, self.map.start_x)
|
||||
self.map.spawn_random_entities(randint(3, 10))
|
||||
self.inventory_menu.update_player(self.player)
|
||||
|
||||
def run(self, screen: Any) -> None:
|
||||
@property
|
||||
def map(self) -> Map:
|
||||
"""
|
||||
Return the current map where the user is.
|
||||
"""
|
||||
return self.maps[self.map_index]
|
||||
|
||||
@map.setter
|
||||
def map(self, m: Map) -> None:
|
||||
"""
|
||||
Redefine the current map.
|
||||
"""
|
||||
if len(self.maps) == self.map_index:
|
||||
# Insert new map
|
||||
self.maps.append(m)
|
||||
# Redefine the current map
|
||||
self.maps[self.map_index] = m
|
||||
|
||||
def run(self, screen: Any) -> None: # pragma no cover
|
||||
"""
|
||||
Main infinite loop.
|
||||
We wait for the player's action, then we do what that should be done
|
||||
when the given key gets pressed.
|
||||
We wait for the player's action, then we do what should be done
|
||||
when a key gets pressed.
|
||||
"""
|
||||
while True: # pragma no cover
|
||||
screen.clear()
|
||||
screen.refresh()
|
||||
screen.refresh()
|
||||
while True:
|
||||
screen.erase()
|
||||
screen.noutrefresh()
|
||||
self.display_actions(DisplayActions.REFRESH)
|
||||
key = screen.getkey()
|
||||
self.handle_key_pressed(
|
||||
KeyValues.translate_key(key, self.settings), key)
|
||||
curses.doupdate()
|
||||
try:
|
||||
key = screen.getkey()
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
return
|
||||
if key == "KEY_MOUSE":
|
||||
_ignored1, x, y, _ignored2, attr = curses.getmouse()
|
||||
self.display_actions(DisplayActions.MOUSE, y, x, attr)
|
||||
else:
|
||||
self.handle_key_pressed(
|
||||
KeyValues.translate_key(key, self.settings), key)
|
||||
|
||||
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
|
||||
-> None:
|
||||
"""
|
||||
Indicates what should be done when the given key is pressed,
|
||||
Indicates what should be done when a given key is pressed,
|
||||
according to the current game state.
|
||||
"""
|
||||
if self.message:
|
||||
self.message = None
|
||||
self.display_actions(DisplayActions.REFRESH)
|
||||
return
|
||||
|
||||
if self.state == GameMode.PLAY:
|
||||
self.handle_key_pressed_play(key)
|
||||
if self.waiting_for_friendly_key:
|
||||
# The player requested to talk with a friendly entity
|
||||
self.handle_friendly_entity_chat(key)
|
||||
elif self.waiting_for_launch_key:
|
||||
# The player requested to launch
|
||||
self.handle_launch(key)
|
||||
else:
|
||||
self.handle_key_pressed_play(key)
|
||||
elif self.state == GameMode.INVENTORY:
|
||||
self.handle_key_pressed_inventory(key)
|
||||
elif self.state == GameMode.MAINMENU:
|
||||
self.handle_key_pressed_main_menu(key)
|
||||
elif self.state == GameMode.SETTINGS:
|
||||
self.settings_menu.handle_key_pressed(key, raw_key, self)
|
||||
elif self.state == GameMode.STORE:
|
||||
self.handle_key_pressed_store(key)
|
||||
elif self.state == GameMode.CHEST:
|
||||
self.handle_key_pressed_chest(key)
|
||||
elif self.state == GameMode.CREDITS:
|
||||
self.state = GameMode.MAINMENU
|
||||
self.display_actions(DisplayActions.REFRESH)
|
||||
|
||||
def handle_key_pressed_play(self, key: KeyValues) -> None:
|
||||
def handle_key_pressed_play(self, key: KeyValues) -> None: # noqa: C901
|
||||
"""
|
||||
In play mode, arrows or zqsd move the main character.
|
||||
"""
|
||||
if key == KeyValues.UP:
|
||||
if self.player.move_up():
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
elif key == KeyValues.DOWN:
|
||||
if self.player.move_down():
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
elif key == KeyValues.LEFT:
|
||||
if self.player.move_left():
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
elif key == KeyValues.RIGHT:
|
||||
if self.player.move_right():
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
elif key == KeyValues.INVENTORY:
|
||||
self.state = GameMode.INVENTORY
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
elif key == KeyValues.USE and self.player.equipped_main:
|
||||
if self.player.equipped_main:
|
||||
self.player.equipped_main.use()
|
||||
if self.player.equipped_secondary:
|
||||
self.player.equipped_secondary.use()
|
||||
elif key == KeyValues.LAUNCH:
|
||||
# Wait for the direction to launch in
|
||||
self.waiting_for_launch_key = True
|
||||
elif key == KeyValues.SPACE:
|
||||
self.state = GameMode.MAINMENU
|
||||
elif key == KeyValues.CHAT:
|
||||
# Wait for the direction of the friendly entity
|
||||
self.waiting_for_friendly_key = True
|
||||
elif key == KeyValues.WAIT:
|
||||
self.map.tick(self.player)
|
||||
elif key == KeyValues.LADDER:
|
||||
self.handle_ladder()
|
||||
elif key == KeyValues.DANCE:
|
||||
self.player.dance()
|
||||
self.map.tick(self.player)
|
||||
|
||||
def handle_ladder(self) -> None:
|
||||
"""
|
||||
The player pressed the ladder key to switch map
|
||||
"""
|
||||
# On a ladder, we switch level
|
||||
y, x = self.player.y, self.player.x
|
||||
if not self.map.tiles[y][x].is_ladder():
|
||||
return
|
||||
|
||||
# We move up on the ladder of the beginning,
|
||||
# down at the end of the stage
|
||||
move_down = y != self.map.start_y or x != self.map.start_x
|
||||
old_map = self.map
|
||||
self.map_index += 1 if move_down else -1
|
||||
if self.map_index == -1:
|
||||
self.map_index = 0
|
||||
return
|
||||
while self.map_index >= len(self.maps):
|
||||
m = broguelike.Generator().run()
|
||||
m.logs = self.logs
|
||||
self.maps.append(m)
|
||||
new_map = self.map
|
||||
new_map.floor = self.map_index
|
||||
old_map.remove_entity(self.player)
|
||||
new_map.add_entity(self.player)
|
||||
if move_down:
|
||||
self.player.move(self.map.start_y, self.map.start_x)
|
||||
self.logs.add_message(
|
||||
_("The player climbs down to the floor {floor}.")
|
||||
.format(floor=-self.map_index))
|
||||
else:
|
||||
# Find the ladder of the end of the game
|
||||
ladder_y, ladder_x = -1, -1
|
||||
for y in range(self.map.height):
|
||||
for x in range(self.map.width):
|
||||
if (y, x) != (self.map.start_y, self.map.start_x) \
|
||||
and self.map.tiles[y][x].is_ladder():
|
||||
ladder_y, ladder_x = y, x
|
||||
break
|
||||
self.player.move(ladder_y, ladder_x)
|
||||
self.logs.add_message(
|
||||
_("The player climbs up the floor {floor}.")
|
||||
.format(floor=-self.map_index))
|
||||
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def handle_friendly_entity_chat(self, key: KeyValues) -> None:
|
||||
"""
|
||||
If the player tries to talk to a friendly entity, the game waits for
|
||||
a directional key to be pressed, verifies there is a friendly entity
|
||||
in that direction and then lets the player interact with it.
|
||||
"""
|
||||
if not self.waiting_for_friendly_key:
|
||||
return
|
||||
self.waiting_for_friendly_key = False
|
||||
|
||||
if key == KeyValues.UP:
|
||||
xp = self.player.x
|
||||
yp = self.player.y - 1
|
||||
elif key == KeyValues.DOWN:
|
||||
xp = self.player.x
|
||||
yp = self.player.y + 1
|
||||
elif key == KeyValues.LEFT:
|
||||
xp = self.player.x - 1
|
||||
yp = self.player.y
|
||||
elif key == KeyValues.RIGHT:
|
||||
xp = self.player.x + 1
|
||||
yp = self.player.y
|
||||
else:
|
||||
return
|
||||
if self.map.entity_is_present(yp, xp):
|
||||
for entity in self.map.entities:
|
||||
if entity.is_friendly() and entity.x == xp and \
|
||||
entity.y == yp:
|
||||
msg = entity.talk_to(self.player)
|
||||
self.logs.add_message(msg)
|
||||
if entity.is_merchant():
|
||||
self.state = GameMode.STORE
|
||||
self.is_in_store_menu = True
|
||||
self.store_menu.update_merchant(entity)
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
elif entity.is_chest():
|
||||
self.state = GameMode.CHEST
|
||||
self.is_in_chest_menu = True
|
||||
self.chest_menu.update_chest(entity)
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def handle_launch(self, key: KeyValues) -> None:
|
||||
"""
|
||||
If the player tries to throw something in a direction, the game looks
|
||||
for entities in that direction and within the range of the player's
|
||||
weapon and adds damage
|
||||
"""
|
||||
if not self.waiting_for_launch_key:
|
||||
return
|
||||
self.waiting_for_launch_key = False
|
||||
|
||||
if key == KeyValues.UP:
|
||||
direction = 0
|
||||
elif key == KeyValues.DOWN:
|
||||
direction = 2
|
||||
elif key == KeyValues.LEFT:
|
||||
direction = 3
|
||||
elif key == KeyValues.RIGHT:
|
||||
direction = 1
|
||||
else:
|
||||
return
|
||||
|
||||
if self.player.equipped_main:
|
||||
if self.player.equipped_main.throw(direction):
|
||||
self.map.tick(self.player)
|
||||
|
||||
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In the inventory menu, we can interact with items or close the menu.
|
||||
"""
|
||||
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
|
||||
self.state = GameMode.PLAY
|
||||
elif key == KeyValues.UP:
|
||||
self.inventory_menu.go_up()
|
||||
elif key == KeyValues.DOWN:
|
||||
self.inventory_menu.go_down()
|
||||
if self.inventory_menu.values and not self.player.dead:
|
||||
if key == KeyValues.USE:
|
||||
self.inventory_menu.validate().use()
|
||||
elif key == KeyValues.EQUIP:
|
||||
item = self.inventory_menu.validate()
|
||||
item.unequip() if item.equipped else item.equip()
|
||||
elif key == KeyValues.DROP:
|
||||
self.inventory_menu.validate().drop()
|
||||
|
||||
# Ensure that the cursor has a good position
|
||||
self.inventory_menu.position = min(self.inventory_menu.position,
|
||||
len(self.inventory_menu.values)
|
||||
- 1)
|
||||
|
||||
def handle_key_pressed_store(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In a store menu, we can buy items or close the menu.
|
||||
"""
|
||||
menu = self.store_menu if self.is_in_store_menu else self.inventory_menu
|
||||
|
||||
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
|
||||
self.state = GameMode.PLAY
|
||||
elif key == KeyValues.UP:
|
||||
menu.go_up()
|
||||
elif key == KeyValues.DOWN:
|
||||
menu.go_down()
|
||||
elif key == KeyValues.LEFT:
|
||||
self.is_in_store_menu = False
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
elif key == KeyValues.RIGHT:
|
||||
self.is_in_store_menu = True
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
if menu.values and not self.player.dead:
|
||||
if key == KeyValues.ENTER:
|
||||
item = menu.validate()
|
||||
owner = self.store_menu.merchant if self.is_in_store_menu \
|
||||
else self.player
|
||||
buyer = self.player if self.is_in_store_menu \
|
||||
else self.store_menu.merchant
|
||||
flag = item.be_sold(buyer, owner)
|
||||
if not flag:
|
||||
self.message = _("The buyer does not have enough money")
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
# Ensure that the cursor has a good position
|
||||
menu.position = min(menu.position, len(menu.values) - 1)
|
||||
|
||||
def handle_key_pressed_chest(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In a chest menu, we can take or put items or close the menu.
|
||||
"""
|
||||
menu = self.chest_menu if self.is_in_chest_menu else self.inventory_menu
|
||||
|
||||
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
|
||||
self.state = GameMode.PLAY
|
||||
elif key == KeyValues.UP:
|
||||
menu.go_up()
|
||||
elif key == KeyValues.DOWN:
|
||||
menu.go_down()
|
||||
elif key == KeyValues.LEFT:
|
||||
self.is_in_chest_menu = False
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
elif key == KeyValues.RIGHT:
|
||||
self.is_in_chest_menu = True
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
if menu.values and not self.player.dead:
|
||||
if key == KeyValues.ENTER:
|
||||
item = menu.validate()
|
||||
owner = self.chest_menu.chest if self.is_in_chest_menu \
|
||||
else self.player
|
||||
buyer = self.player if self.is_in_chest_menu \
|
||||
else self.chest_menu.chest
|
||||
item.be_sold(buyer, owner, for_free=True)
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
# Ensure that the cursor has a good position
|
||||
menu.position = min(menu.position, len(menu.values) - 1)
|
||||
|
||||
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
|
||||
"""
|
||||
In the main menu, we can navigate through options.
|
||||
In the main menu, we can navigate through different options.
|
||||
"""
|
||||
if key == KeyValues.DOWN:
|
||||
self.main_menu.go_down()
|
||||
@ -122,31 +406,63 @@ class Game:
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the game to a dictionary
|
||||
Saves the game to a dictionary.
|
||||
"""
|
||||
return self.map.save_state()
|
||||
return dict(map_index=self.map_index,
|
||||
maps=[m.save_state() for m in self.maps])
|
||||
|
||||
def load_state(self, d: dict) -> None:
|
||||
"""
|
||||
Loads the game from a dictionary
|
||||
Loads the game from a dictionary.
|
||||
"""
|
||||
self.map.load_state(d)
|
||||
# noinspection PyTypeChecker
|
||||
self.player = self.map.find_entities(Player)[0]
|
||||
try:
|
||||
self.map_index = d["map_index"]
|
||||
self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
|
||||
for i, m in enumerate(self.maps):
|
||||
m.floor = i
|
||||
m.logs = self.logs
|
||||
except KeyError as error:
|
||||
self.message = _("Some keys are missing in your save file.\n"
|
||||
"Your save seems to be corrupt. It got deleted.")\
|
||||
+ f"\n{error}"
|
||||
os.unlink(ResourceManager.get_config_path("save.json"))
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
return
|
||||
|
||||
players = self.map.find_entities(Player)
|
||||
if not players:
|
||||
self.message = _("No player was found on this map!\n"
|
||||
"Maybe you died?")
|
||||
self.player.health = 0
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
return
|
||||
|
||||
self.player = players[0]
|
||||
self.inventory_menu.update_player(self.player)
|
||||
self.map.compute_visibility(self.player.y, self.player.x,
|
||||
self.player.vision)
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def load_game(self) -> None:
|
||||
"""
|
||||
Loads the game from a file
|
||||
Loads the game from a file.
|
||||
"""
|
||||
file_path = ResourceManager.get_config_path("save.json")
|
||||
if os.path.isfile(file_path):
|
||||
with open(file_path, "r") as f:
|
||||
self.load_state(json.loads(f.read()))
|
||||
try:
|
||||
state = json.loads(f.read())
|
||||
self.load_state(state)
|
||||
except JSONDecodeError:
|
||||
self.message = _("The JSON file is not correct.\n"
|
||||
"Your save seems corrupted. "
|
||||
"It got deleted.")
|
||||
os.unlink(file_path)
|
||||
self.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def save_game(self) -> None:
|
||||
"""
|
||||
Saves the game to a file
|
||||
Saves the game to a file.
|
||||
"""
|
||||
with open(ResourceManager.get_config_path("save.json"), "w") as f:
|
||||
f.write(json.dumps(self.save_state()))
|
||||
|
@ -1,15 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
from enum import Enum, auto
|
||||
from math import sqrt
|
||||
from random import choice, randint
|
||||
from typing import List, Optional
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from copy import deepcopy
|
||||
from enum import auto, Enum
|
||||
from functools import reduce
|
||||
from itertools import product
|
||||
from math import ceil, sqrt
|
||||
from queue import PriorityQueue
|
||||
from random import choice, randint
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from .display.texturepack import TexturePack
|
||||
from .translations import gettext as _
|
||||
|
||||
|
||||
class Logs:
|
||||
"""
|
||||
The logs object stores the messages to display. It is encapsulating a list
|
||||
The logs object stores the messages to display. It encapsulates a list
|
||||
of such messages, to allow multiple pointers to keep track of it even if
|
||||
the list was to be reassigned.
|
||||
"""
|
||||
@ -27,16 +34,47 @@ class Logs:
|
||||
self.messages = []
|
||||
|
||||
|
||||
class Slope():
|
||||
X: int
|
||||
Y: int
|
||||
|
||||
def __init__(self, y: int, x: int) -> None:
|
||||
self.Y = y
|
||||
self.X = x
|
||||
|
||||
def compare(self, other: "Slope") -> int:
|
||||
y, x = other.Y, other.X
|
||||
return self.Y * x - self.X * y
|
||||
|
||||
def __lt__(self, other: "Slope") -> bool:
|
||||
return self.compare(other) < 0
|
||||
|
||||
def __eq__(self, other: "Slope") -> bool:
|
||||
return self.compare(other) == 0
|
||||
|
||||
def __gt__(self, other: "Slope") -> bool:
|
||||
return self.compare(other) > 0
|
||||
|
||||
def __le__(self, other: "Slope") -> bool:
|
||||
return self.compare(other) <= 0
|
||||
|
||||
def __ge__(self, other: "Slope") -> bool:
|
||||
return self.compare(other) >= 0
|
||||
|
||||
|
||||
class Map:
|
||||
"""
|
||||
Object that represents a Map with its width, height
|
||||
The Map object represents a with its width, height
|
||||
and tiles, that have their custom properties.
|
||||
"""
|
||||
floor: int
|
||||
width: int
|
||||
height: int
|
||||
start_y: int
|
||||
start_x: int
|
||||
tiles: List[List["Tile"]]
|
||||
visibility: List[List[bool]]
|
||||
seen_tiles: List[List[bool]]
|
||||
entities: List["Entity"]
|
||||
logs: Logs
|
||||
# coordinates of the point that should be
|
||||
@ -44,28 +82,39 @@ class Map:
|
||||
currentx: int
|
||||
currenty: int
|
||||
|
||||
def __init__(self, width: int, height: int, tiles: list,
|
||||
start_y: int, start_x: int):
|
||||
def __init__(self, width: int = 0, height: int = 0, tiles: list = None,
|
||||
start_y: int = 0, start_x: int = 0):
|
||||
self.floor = 0
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.start_y = start_y
|
||||
self.start_x = start_x
|
||||
self.tiles = tiles
|
||||
self.currenty = start_y
|
||||
self.currentx = start_x
|
||||
self.tiles = tiles or []
|
||||
self.visibility = [[False for _ in range(len(self.tiles[0]))]
|
||||
for _ in range(len(self.tiles))]
|
||||
self.seen_tiles = [[False for _ in range(len(tiles[0]))]
|
||||
for _ in range(len(self.tiles))]
|
||||
self.entities = []
|
||||
self.logs = Logs()
|
||||
|
||||
def add_entity(self, entity: "Entity") -> None:
|
||||
"""
|
||||
Register a new entity in the map.
|
||||
Registers a new entity in the map.
|
||||
"""
|
||||
self.entities.append(entity)
|
||||
if entity.is_familiar():
|
||||
self.entities.insert(1, entity)
|
||||
else:
|
||||
self.entities.append(entity)
|
||||
entity.map = self
|
||||
|
||||
def remove_entity(self, entity: "Entity") -> None:
|
||||
"""
|
||||
Unregister an entity from the map.
|
||||
Unregisters an entity from the map.
|
||||
"""
|
||||
self.entities.remove(entity)
|
||||
if entity in self.entities:
|
||||
self.entities.remove(entity)
|
||||
|
||||
def find_entities(self, entity_class: type) -> list:
|
||||
return [entity for entity in self.entities
|
||||
@ -73,16 +122,26 @@ class Map:
|
||||
|
||||
def is_free(self, y: int, x: int) -> bool:
|
||||
"""
|
||||
Indicates that the case at the coordinates (y, x) is empty.
|
||||
Indicates that the tile at the coordinates (y, x) is empty.
|
||||
"""
|
||||
return 0 <= y < self.height and 0 <= x < self.width and \
|
||||
self.tiles[y][x].can_walk() and \
|
||||
not any(entity.x == x and entity.y == y for entity in self.entities)
|
||||
|
||||
def entity_is_present(self, y: int, x: int) -> bool:
|
||||
"""
|
||||
Indicates that the tile at the coordinates (y, x) contains a killable
|
||||
entity.
|
||||
"""
|
||||
return 0 <= y < self.height and 0 <= x < self.width and \
|
||||
any(entity.x == x and entity.y == y and entity.is_friendly()
|
||||
for entity in self.entities)
|
||||
|
||||
@staticmethod
|
||||
def load(filename: str) -> "Map":
|
||||
"""
|
||||
Read a file that contains the content of a map, and build a Map object.
|
||||
Reads a file that contains the content of a map,
|
||||
and builds a Map object.
|
||||
"""
|
||||
with open(filename, "r") as f:
|
||||
file = f.read()
|
||||
@ -91,7 +150,7 @@ class Map:
|
||||
@staticmethod
|
||||
def load_from_string(content: str) -> "Map":
|
||||
"""
|
||||
Load a map represented by its characters and build a Map object.
|
||||
Loads a map represented by its characters and builds a Map object.
|
||||
"""
|
||||
lines = content.split("\n")
|
||||
first_line = lines[0]
|
||||
@ -107,7 +166,7 @@ class Map:
|
||||
@staticmethod
|
||||
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
|
||||
"""
|
||||
Transforms a string into the list of corresponding tiles
|
||||
Transforms a string into the list of corresponding tiles.
|
||||
"""
|
||||
lines = content.split("\n")
|
||||
tiles = [[Tile.from_ascii_char(c)
|
||||
@ -116,37 +175,155 @@ class Map:
|
||||
|
||||
def draw_string(self, pack: TexturePack) -> str:
|
||||
"""
|
||||
Draw the current map as a string object that can be rendered
|
||||
Draws the current map as a string object that can be rendered
|
||||
in the window.
|
||||
"""
|
||||
return "\n".join("".join(tile.char(pack) for tile in line)
|
||||
for line in self.tiles)
|
||||
|
||||
def spawn_random_entities(self, count: int) -> None:
|
||||
"""
|
||||
Put randomly {count} hedgehogs on the map, where it is available.
|
||||
"""
|
||||
for _ in range(count):
|
||||
y, x = 0, 0
|
||||
while True:
|
||||
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
|
||||
tile = self.tiles[y][x]
|
||||
if tile.can_walk():
|
||||
break
|
||||
entity = choice(Entity.get_all_entity_classes())()
|
||||
entity.move(y, x)
|
||||
self.add_entity(entity)
|
||||
def is_visible_from(self, starty: int, startx: int, desty: int, destx: int,
|
||||
max_range: int) -> bool:
|
||||
oldvisibility = deepcopy(self.visibility)
|
||||
oldseen = deepcopy(self.seen_tiles)
|
||||
self.compute_visibility(starty, startx, max_range)
|
||||
result = self.visibility[desty][destx]
|
||||
self.visibility = oldvisibility
|
||||
self.seen_tiles = oldseen
|
||||
return result
|
||||
|
||||
def tick(self) -> None:
|
||||
def compute_visibility(self, y: int, x: int, max_range: int) -> None:
|
||||
"""
|
||||
Trigger all entity events.
|
||||
Sets the visible tiles to be the ones visible by an entity at point
|
||||
(y, x), using a twaked shadow casting algorithm
|
||||
"""
|
||||
|
||||
for line in self.visibility:
|
||||
for i in range(len(line)):
|
||||
line[i] = False
|
||||
self.set_visible(0, 0, 0, (y, x))
|
||||
for octant in range(8):
|
||||
self.compute_visibility_octant(octant, (y, x), max_range, 1,
|
||||
Slope(1, 1), Slope(0, 1))
|
||||
|
||||
def crop_top_visibility(self, octant: int, origin: Tuple[int, int],
|
||||
x: int, top: Slope) -> int:
|
||||
if top.X == 1:
|
||||
top_y = x
|
||||
else:
|
||||
top_y = ceil(((x * 2 - 1) * top.Y + top.X) / (top.X * 2))
|
||||
if self.is_wall(top_y, x, octant, origin):
|
||||
top_y += top >= Slope(top_y * 2 + 1, x * 2) and not \
|
||||
self.is_wall(top_y + 1, x, octant, origin)
|
||||
else:
|
||||
ax = x * 2
|
||||
ax += self.is_wall(top_y + 1, x + 1, octant, origin)
|
||||
top_y += top > Slope(top_y * 2 + 1, ax)
|
||||
return top_y
|
||||
|
||||
def crop_bottom_visibility(self, octant: int, origin: Tuple[int, int],
|
||||
x: int, bottom: Slope) -> int:
|
||||
if bottom.Y == 0:
|
||||
bottom_y = 0
|
||||
else:
|
||||
bottom_y = ceil(((x * 2 - 1) * bottom.Y + bottom.X)
|
||||
/ (bottom.X * 2))
|
||||
bottom_y += bottom >= Slope(bottom_y * 2 + 1, x * 2) and \
|
||||
self.is_wall(bottom_y, x, octant, origin) and \
|
||||
not self.is_wall(bottom_y + 1, x, octant, origin)
|
||||
return bottom_y
|
||||
|
||||
def compute_visibility_octant(self, octant: int, origin: Tuple[int, int],
|
||||
max_range: int, distance: int, top: Slope,
|
||||
bottom: Slope) -> None:
|
||||
for x in range(distance, max_range + 1):
|
||||
top_y = self.crop_top_visibility(octant, origin, x, top)
|
||||
bottom_y = self.crop_bottom_visibility(octant, origin, x, bottom)
|
||||
was_opaque = -1
|
||||
for y in range(top_y, bottom_y - 1, -1):
|
||||
if x + y > max_range:
|
||||
continue
|
||||
is_opaque = self.is_wall(y, x, octant, origin)
|
||||
is_visible = is_opaque\
|
||||
or ((y != top_y or top >= Slope(y, x))
|
||||
and (y != bottom_y
|
||||
or bottom <= Slope(y, x)))
|
||||
# is_visible = is_opaque\
|
||||
# or ((y != top_y or top >= Slope(y, x))
|
||||
# and (y != bottom_y or bottom <= Slope(y, x)))
|
||||
if is_visible:
|
||||
self.set_visible(y, x, octant, origin)
|
||||
if x == max_range:
|
||||
continue
|
||||
if is_opaque and was_opaque == 0:
|
||||
nx, ny = x * 2, y * 2 + 1
|
||||
nx -= self.is_wall(y + 1, x, octant, origin)
|
||||
if top > Slope(ny, nx):
|
||||
if y == bottom_y:
|
||||
bottom = Slope(ny, nx)
|
||||
break
|
||||
else:
|
||||
self.compute_visibility_octant(
|
||||
octant, origin, max_range, x + 1, top,
|
||||
Slope(ny, nx))
|
||||
elif y == bottom_y: # pragma: no cover
|
||||
return
|
||||
elif not is_opaque and was_opaque == 1:
|
||||
nx, ny = x * 2, y * 2 + 1
|
||||
nx += self.is_wall(y + 1, x + 1, octant, origin)
|
||||
if bottom >= Slope(ny, nx): # pragma: no cover
|
||||
return
|
||||
top = Slope(ny, nx)
|
||||
was_opaque = is_opaque
|
||||
if was_opaque != 0:
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def translate_coord(y: int, x: int, octant: int,
|
||||
origin: Tuple[int, int]) -> Tuple[int, int]:
|
||||
ny, nx = origin
|
||||
if octant == 0:
|
||||
return ny - y, nx + x
|
||||
elif octant == 1:
|
||||
return ny - x, nx + y
|
||||
elif octant == 2:
|
||||
return ny - x, nx - y
|
||||
elif octant == 3:
|
||||
return ny - y, nx - x
|
||||
elif octant == 4:
|
||||
return ny + y, nx - x
|
||||
elif octant == 5:
|
||||
return ny + x, nx - y
|
||||
elif octant == 6:
|
||||
return ny + x, nx + y
|
||||
elif octant == 7:
|
||||
return ny + y, nx + x
|
||||
|
||||
def is_wall(self, y: int, x: int, octant: int,
|
||||
origin: Tuple[int, int]) -> bool:
|
||||
y, x = self.translate_coord(y, x, octant, origin)
|
||||
return 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]) and \
|
||||
self.tiles[y][x].is_wall()
|
||||
|
||||
def set_visible(self, y: int, x: int, octant: int,
|
||||
origin: Tuple[int, int]) -> None:
|
||||
y, x = self.translate_coord(y, x, octant, origin)
|
||||
if 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]):
|
||||
self.visibility[y][x] = True
|
||||
self.seen_tiles[y][x] = True
|
||||
|
||||
def tick(self, p: Any) -> None:
|
||||
"""
|
||||
Triggers all entity events.
|
||||
"""
|
||||
for entity in self.entities:
|
||||
entity.act(self)
|
||||
if entity.is_familiar():
|
||||
entity.act(p, self)
|
||||
else:
|
||||
entity.act(self)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the map's attributes to a dictionary
|
||||
Saves the map's attributes to a dictionary.
|
||||
"""
|
||||
d = dict()
|
||||
d["width"] = self.width
|
||||
@ -159,11 +336,12 @@ class Map:
|
||||
for enti in self.entities:
|
||||
d["entities"].append(enti.save_state())
|
||||
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
|
||||
d["seen_tiles"] = self.seen_tiles
|
||||
return d
|
||||
|
||||
def load_state(self, d: dict) -> None:
|
||||
def load_state(self, d: dict) -> "Map":
|
||||
"""
|
||||
Loads the map's attributes from a dictionary
|
||||
Loads the map's attributes from a dictionary.
|
||||
"""
|
||||
self.width = d["width"]
|
||||
self.height = d["height"]
|
||||
@ -172,24 +350,52 @@ class Map:
|
||||
self.currentx = d["currentx"]
|
||||
self.currenty = d["currenty"]
|
||||
self.tiles = self.load_dungeon_from_string(d["map"])
|
||||
self.seen_tiles = d["seen_tiles"]
|
||||
self.visibility = [[False for _ in range(len(self.tiles[0]))]
|
||||
for _ in range(len(self.tiles))]
|
||||
self.entities = []
|
||||
dictclasses = Entity.get_all_entity_classes_in_a_dict()
|
||||
for entisave in d["entities"]:
|
||||
self.add_entity(dictclasses[entisave["type"]](**entisave))
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def neighbourhood(grid: List[List["Tile"]], y: int, x: int,
|
||||
large: bool = False, oob: bool = False) \
|
||||
-> List[List[int]]:
|
||||
"""
|
||||
Returns up to 8 nearby coordinates, in a 3x3 square around the input
|
||||
coordinate if large is set to True, or in a 5-square cross by default.
|
||||
Does not return coordinates if they are out of bounds.
|
||||
"""
|
||||
height, width = len(grid), len(grid[0])
|
||||
neighbours = []
|
||||
if large:
|
||||
dyxs = [[dy, dx] for dy, dx in product([-1, 0, 1], [-1, 0, 1])]
|
||||
dyxs = dyxs[:5] + dyxs[6:]
|
||||
else:
|
||||
dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]]
|
||||
for dy, dx in dyxs:
|
||||
if oob or (0 <= y + dy < height and 0 <= x + dx < width):
|
||||
neighbours.append([y + dy, x + dx])
|
||||
return neighbours
|
||||
|
||||
|
||||
class Tile(Enum):
|
||||
"""
|
||||
The internal representation of the tiles of the map
|
||||
The internal representation of the tiles of the map.
|
||||
"""
|
||||
EMPTY = auto()
|
||||
WALL = auto()
|
||||
FLOOR = auto()
|
||||
LADDER = auto()
|
||||
DOOR = auto()
|
||||
|
||||
@staticmethod
|
||||
def from_ascii_char(ch: str) -> "Tile":
|
||||
"""
|
||||
Maps an ascii character to its equivalent in the texture pack
|
||||
Maps an ascii character to its equivalent in the texture pack.
|
||||
"""
|
||||
for tile in Tile:
|
||||
if tile.char(TexturePack.ASCII_PACK) == ch:
|
||||
@ -199,31 +405,56 @@ class Tile(Enum):
|
||||
def char(self, pack: TexturePack) -> str:
|
||||
"""
|
||||
Translates a Tile to the corresponding character according
|
||||
to the texture pack
|
||||
to the texture pack.
|
||||
"""
|
||||
return getattr(pack, self.name)
|
||||
val = getattr(pack, self.name)
|
||||
return val[0] if isinstance(val, tuple) else val
|
||||
|
||||
def visible_color(self, pack: TexturePack) -> Tuple[int, int]:
|
||||
"""
|
||||
Retrieve the tuple (fg_color, bg_color) of the current Tile
|
||||
if it is visible.
|
||||
"""
|
||||
val = getattr(pack, self.name)
|
||||
return (val[2], val[4]) if isinstance(val, tuple) else \
|
||||
(pack.tile_fg_visible_color, pack.tile_bg_color)
|
||||
|
||||
def hidden_color(self, pack: TexturePack) -> Tuple[int, int]:
|
||||
"""
|
||||
Retrieve the tuple (fg_color, bg_color) of the current Tile.
|
||||
"""
|
||||
val = getattr(pack, self.name)
|
||||
return (val[1], val[3]) if isinstance(val, tuple) else \
|
||||
(pack.tile_fg_color, pack.tile_bg_color)
|
||||
|
||||
def is_wall(self) -> bool:
|
||||
"""
|
||||
Is this Tile a wall?
|
||||
"""
|
||||
return self == Tile.WALL
|
||||
return self == Tile.WALL or self == Tile.DOOR
|
||||
|
||||
def is_ladder(self) -> bool:
|
||||
"""
|
||||
Is this Tile a ladder?
|
||||
"""
|
||||
return self == Tile.LADDER
|
||||
|
||||
def can_walk(self) -> bool:
|
||||
"""
|
||||
Check if an entity (player or not) can move in this tile.
|
||||
Checks if an entity (player or not) can move in this tile.
|
||||
"""
|
||||
return not self.is_wall() and self != Tile.EMPTY
|
||||
|
||||
|
||||
class Entity:
|
||||
"""
|
||||
An Entity object represents any entity present on the map
|
||||
An Entity object represents any entity present on the map.
|
||||
"""
|
||||
y: int
|
||||
x: int
|
||||
name: str
|
||||
map: Map
|
||||
paths: Dict[Tuple[int, int], Tuple[int, int]]
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
|
||||
@ -232,11 +463,12 @@ class Entity:
|
||||
self.x = x
|
||||
self.name = name
|
||||
self.map = map
|
||||
self.paths = None
|
||||
|
||||
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
|
||||
-> bool:
|
||||
"""
|
||||
Checks if moving to (y,x) is authorized
|
||||
Checks if moving to (y,x) is authorized.
|
||||
"""
|
||||
free = self.map.is_free(y, x)
|
||||
if free and move_if_possible:
|
||||
@ -245,7 +477,7 @@ class Entity:
|
||||
|
||||
def move(self, y: int, x: int) -> bool:
|
||||
"""
|
||||
Moves an entity to (y,x) coordinates
|
||||
Moves an entity to (y,x) coordinates.
|
||||
"""
|
||||
self.y = y
|
||||
self.x = x
|
||||
@ -253,49 +485,100 @@ class Entity:
|
||||
|
||||
def move_up(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity up one tile, if possible
|
||||
Moves the entity up one tile, if possible.
|
||||
"""
|
||||
return self.move(self.y - 1, self.x) if force else \
|
||||
self.check_move(self.y - 1, self.x, True)
|
||||
|
||||
def move_down(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity down one tile, if possible
|
||||
Moves the entity down one tile, if possible.
|
||||
"""
|
||||
return self.move(self.y + 1, self.x) if force else \
|
||||
self.check_move(self.y + 1, self.x, True)
|
||||
|
||||
def move_left(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity left one tile, if possible
|
||||
Moves the entity left one tile, if possible.
|
||||
"""
|
||||
return self.move(self.y, self.x - 1) if force else \
|
||||
self.check_move(self.y, self.x - 1, True)
|
||||
|
||||
def move_right(self, force: bool = False) -> bool:
|
||||
"""
|
||||
Moves the entity right one tile, if possible
|
||||
Moves the entity right one tile, if possible.
|
||||
"""
|
||||
return self.move(self.y, self.x + 1) if force else \
|
||||
self.check_move(self.y, self.x + 1, True)
|
||||
|
||||
def recalculate_paths(self, max_distance: int = 12) -> None:
|
||||
"""
|
||||
Uses Dijkstra algorithm to calculate best paths for other entities to
|
||||
go to this entity. If self.paths is None, does nothing.
|
||||
"""
|
||||
if self.paths is None:
|
||||
return
|
||||
distances = []
|
||||
predecessors = []
|
||||
# four Dijkstras, one for each adjacent tile
|
||||
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
||||
queue = PriorityQueue()
|
||||
new_y, new_x = self.y + dir_y, self.x + dir_x
|
||||
if not 0 <= new_y < self.map.height or \
|
||||
not 0 <= new_x < self.map.width or \
|
||||
not self.map.tiles[new_y][new_x].can_walk():
|
||||
continue
|
||||
queue.put(((1, 0), (new_y, new_x)))
|
||||
visited = [(self.y, self.x)]
|
||||
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
|
||||
predecessors.append({(new_y, new_x): (self.y, self.x)})
|
||||
while not queue.empty():
|
||||
dist, (y, x) = queue.get()
|
||||
if dist[0] >= max_distance or (y, x) in visited:
|
||||
continue
|
||||
visited.append((y, x))
|
||||
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
||||
new_y, new_x = y + diff_y, x + diff_x
|
||||
if not 0 <= new_y < self.map.height or \
|
||||
not 0 <= new_x < self.map.width or \
|
||||
not self.map.tiles[new_y][new_x].can_walk():
|
||||
continue
|
||||
new_distance = (dist[0] + 1,
|
||||
dist[1] + (not self.map.is_free(y, x)))
|
||||
if not (new_y, new_x) in distances[-1] or \
|
||||
distances[-1][(new_y, new_x)] > new_distance:
|
||||
predecessors[-1][(new_y, new_x)] = (y, x)
|
||||
distances[-1][(new_y, new_x)] = new_distance
|
||||
queue.put((new_distance, (new_y, new_x)))
|
||||
# For each tile that is reached by at least one Dijkstra, sort the
|
||||
# different paths by distance to the player. For the technical bits :
|
||||
# The reduce function is a fold starting on the first element of the
|
||||
# iterable, and we associate the points to their distance, sort
|
||||
# along the distance, then only keep the points.
|
||||
self.paths = {}
|
||||
for y, x in reduce(set.union,
|
||||
[set(p.keys()) for p in predecessors], set()):
|
||||
self.paths[(y, x)] = [p for d, p in sorted(
|
||||
[(distances[i][(y, x)], predecessors[i][(y, x)])
|
||||
for i in range(len(distances)) if (y, x) in predecessors[i]])]
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
Define the action of the entity that is ran each tick.
|
||||
Defines the action the entity will do at each tick.
|
||||
By default, does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def distance_squared(self, other: "Entity") -> int:
|
||||
"""
|
||||
Get the square of the distance to another entity.
|
||||
Useful to check distances since square root takes time.
|
||||
Gives the square of the distance to another entity.
|
||||
Useful to check distances since taking the square root takes time.
|
||||
"""
|
||||
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
|
||||
|
||||
def distance(self, other: "Entity") -> float:
|
||||
"""
|
||||
Get the cartesian distance to another entity.
|
||||
Gives the cartesian distance to another entity.
|
||||
"""
|
||||
return sqrt(self.distance_squared(other))
|
||||
|
||||
@ -312,38 +595,107 @@ class Entity:
|
||||
from squirrelbattle.entities.items import Item
|
||||
return isinstance(self, Item)
|
||||
|
||||
def is_friendly(self) -> bool:
|
||||
"""
|
||||
Is this entity a friendly entity?
|
||||
"""
|
||||
return isinstance(self, FriendlyEntity)
|
||||
|
||||
def is_familiar(self) -> bool:
|
||||
"""
|
||||
Is this entity a familiar?
|
||||
"""
|
||||
from squirrelbattle.entities.friendly import Familiar
|
||||
return isinstance(self, Familiar)
|
||||
|
||||
def is_merchant(self) -> bool:
|
||||
"""
|
||||
Is this entity a merchant?
|
||||
"""
|
||||
from squirrelbattle.entities.friendly import Merchant
|
||||
return isinstance(self, Merchant)
|
||||
|
||||
def is_chest(self) -> bool:
|
||||
"""
|
||||
Is this entity a chest?
|
||||
"""
|
||||
from squirrelbattle.entities.friendly import Chest
|
||||
return isinstance(self, Chest)
|
||||
|
||||
@property
|
||||
def translated_name(self) -> str:
|
||||
"""
|
||||
Translates the name of entities.
|
||||
"""
|
||||
return _(self.name.replace("_", " "))
|
||||
|
||||
@staticmethod
|
||||
def get_all_entity_classes():
|
||||
def get_all_entity_classes() -> list:
|
||||
"""
|
||||
Returns all entities subclasses
|
||||
Returns all entities subclasses.
|
||||
"""
|
||||
from squirrelbattle.entities.items import Heart, Bomb
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, \
|
||||
Rabbit, TeddyBear
|
||||
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
|
||||
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
|
||||
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
|
||||
Rabbit, TeddyBear, GiantSeaEagle
|
||||
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
|
||||
Trumpet, Chest
|
||||
return [BodySnatchPotion, Bomb, Chest, GiantSeaEagle, Heart,
|
||||
Hedgehog, Merchant, Rabbit, Sunflower, TeddyBear, Tiger,
|
||||
Trumpet]
|
||||
|
||||
@staticmethod
|
||||
def get_weights() -> list:
|
||||
"""
|
||||
Returns a weigth list associated to the above function, to
|
||||
be used to spawn random entities with a certain probability.
|
||||
"""
|
||||
return [30, 80, 50, 1, 100, 100, 60, 70, 70, 20, 40, 40]
|
||||
|
||||
@staticmethod
|
||||
def get_all_entity_classes_in_a_dict() -> dict:
|
||||
"""
|
||||
Returns all entities subclasses in a dictionary
|
||||
Returns all entities subclasses in a dictionary.
|
||||
"""
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \
|
||||
TeddyBear
|
||||
from squirrelbattle.entities.items import Bomb, Heart
|
||||
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
|
||||
TeddyBear, GiantSeaEagle
|
||||
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
|
||||
Trumpet, Chest
|
||||
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
|
||||
Heart, Sword, Shield, Chestplate, Helmet, RingCritical, RingXP, \
|
||||
ScrollofDamage, ScrollofWeakening, Ruler, Bow, FireBallStaff, \
|
||||
Monocle
|
||||
return {
|
||||
"Beaver": Beaver,
|
||||
"BodySnatchPotion": BodySnatchPotion,
|
||||
"Bomb": Bomb,
|
||||
"Bow": Bow,
|
||||
"Chest": Chest,
|
||||
"Chestplate": Chestplate,
|
||||
"FireBallStaff": FireBallStaff,
|
||||
"GiantSeaEagle": GiantSeaEagle,
|
||||
"Heart": Heart,
|
||||
"Hedgehog": Hedgehog,
|
||||
"Rabbit": Rabbit,
|
||||
"TeddyBear": TeddyBear,
|
||||
"Helmet": Helmet,
|
||||
"Merchant": Merchant,
|
||||
"Monocle": Monocle,
|
||||
"Player": Player,
|
||||
"Rabbit": Rabbit,
|
||||
"RingCritical": RingCritical,
|
||||
"RingXP": RingXP,
|
||||
"Ruler": Ruler,
|
||||
"ScrollofDamage": ScrollofDamage,
|
||||
"ScrollofWeakening": ScrollofWeakening,
|
||||
"Shield": Shield,
|
||||
"Sunflower": Sunflower,
|
||||
"Sword": Sword,
|
||||
"Trumpet": Trumpet,
|
||||
"TeddyBear": TeddyBear,
|
||||
"Tiger": Tiger,
|
||||
}
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the coordinates of the entity
|
||||
Saves the coordinates of the entity.
|
||||
"""
|
||||
d = dict()
|
||||
d["x"] = self.x
|
||||
@ -355,7 +707,7 @@ class Entity:
|
||||
class FightingEntity(Entity):
|
||||
"""
|
||||
A FightingEntity is an entity that can fight, and thus has a health,
|
||||
level and stats
|
||||
level and stats.
|
||||
"""
|
||||
maxhealth: int
|
||||
health: int
|
||||
@ -365,11 +717,13 @@ class FightingEntity(Entity):
|
||||
dexterity: int
|
||||
constitution: int
|
||||
level: int
|
||||
critical: int
|
||||
confused: int # Seulement 0 ou 1
|
||||
|
||||
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
|
||||
strength: int = 0, intelligence: int = 0, charisma: int = 0,
|
||||
dexterity: int = 0, constitution: int = 0, level: int = 0,
|
||||
*args, **kwargs) -> None:
|
||||
critical: int = 0, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.maxhealth = maxhealth
|
||||
self.health = maxhealth if health is None else health
|
||||
@ -379,46 +733,162 @@ class FightingEntity(Entity):
|
||||
self.dexterity = dexterity
|
||||
self.constitution = constitution
|
||||
self.level = level
|
||||
self.critical = critical
|
||||
self.effects = [] # effects = temporary buff or weakening of the stats.
|
||||
self.confused = 0
|
||||
|
||||
@property
|
||||
def dead(self) -> bool:
|
||||
"""
|
||||
Is this entity dead ?
|
||||
"""
|
||||
return self.health <= 0
|
||||
|
||||
def act(self, m: Map) -> None:
|
||||
"""
|
||||
Refreshes all current effects.
|
||||
"""
|
||||
for i in range(len(self.effects)):
|
||||
self.effects[i][2] -= 1
|
||||
|
||||
copy = self.effects[:]
|
||||
for i in range(len(copy)):
|
||||
if copy[i][2] <= 0:
|
||||
setattr(self, copy[i][0],
|
||||
getattr(self, copy[i][0]) - copy[i][1])
|
||||
self.effects.remove(copy[i])
|
||||
|
||||
def hit(self, opponent: "FightingEntity") -> str:
|
||||
"""
|
||||
Deals damage to the opponent, based on the stats
|
||||
The entity deals damage to the opponent
|
||||
based on their respective stats.
|
||||
"""
|
||||
return f"{self.name} hits {opponent.name}. "\
|
||||
+ opponent.take_damage(self, self.strength)
|
||||
if self.confused:
|
||||
return _("{name} is confused, it can not hit {opponent}.")\
|
||||
.format(name=_(self.translated_name.capitalize()),
|
||||
opponent=_(opponent.translated_name))
|
||||
diceroll = randint(1, 100)
|
||||
damage = max(0, self.strength)
|
||||
string = " "
|
||||
if diceroll <= self.critical: # It is a critical hit
|
||||
damage *= 4
|
||||
string = " " + _("It's a critical hit!") + " "
|
||||
return _("{name} hits {opponent}.")\
|
||||
.format(name=_(self.translated_name.capitalize()),
|
||||
opponent=_(opponent.translated_name)) + string + \
|
||||
opponent.take_damage(self, damage)
|
||||
|
||||
def take_damage(self, attacker: "Entity", amount: int) -> str:
|
||||
"""
|
||||
Take damage from the attacker, based on the stats
|
||||
The entity takes damage from the attacker
|
||||
based on their respective stats.
|
||||
"""
|
||||
self.health -= amount
|
||||
damage = 0
|
||||
if amount != 0:
|
||||
damage = max(1, amount - self.constitution)
|
||||
self.health -= damage
|
||||
if self.health <= 0:
|
||||
self.die()
|
||||
return f"{self.name} takes {amount} damage."\
|
||||
+ (f" {self.name} dies." if self.health <= 0 else "")
|
||||
return _("{name} takes {damage} damage.")\
|
||||
.format(name=self.translated_name.capitalize(), damage=str(damage))\
|
||||
+ (" " + _("{name} dies.")
|
||||
.format(name=self.translated_name.capitalize())
|
||||
if self.health <= 0 else "")
|
||||
|
||||
def die(self) -> None:
|
||||
"""
|
||||
If a fighting entity has no more health, it dies and is removed
|
||||
If a fighting entity has no more health, it dies and is removed.
|
||||
"""
|
||||
self.map.remove_entity(self)
|
||||
|
||||
def keys(self) -> list:
|
||||
"""
|
||||
Returns a fighting entities specific attributes
|
||||
Returns a fighting entity's specific attributes.
|
||||
"""
|
||||
return ["maxhealth", "health", "level", "strength",
|
||||
return ["name", "maxhealth", "health", "level", "strength",
|
||||
"intelligence", "charisma", "dexterity", "constitution"]
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Saves the state of the entity into a dictionary
|
||||
Saves the state of the entity into a dictionary.
|
||||
"""
|
||||
d = super().save_state()
|
||||
for name in self.keys():
|
||||
d[name] = getattr(self, name)
|
||||
return d
|
||||
|
||||
|
||||
class FriendlyEntity(FightingEntity):
|
||||
"""
|
||||
Friendly entities are living entities which do not attack the player.
|
||||
"""
|
||||
dialogue_option: list
|
||||
|
||||
def talk_to(self, player: Any) -> str:
|
||||
return _("{entity} said: {message}").format(
|
||||
entity=self.translated_name.capitalize(),
|
||||
message=choice(self.dialogue_option))
|
||||
|
||||
def keys(self) -> list:
|
||||
"""
|
||||
Returns a friendly entity's specific attributes.
|
||||
"""
|
||||
return ["maxhealth", "health"]
|
||||
|
||||
|
||||
class InventoryHolder(Entity):
|
||||
hazel: int # Currency of the game
|
||||
inventory: list
|
||||
|
||||
def translate_inventory(self, inventory: list) -> list:
|
||||
"""
|
||||
Translates the JSON save of the inventory into a list of the items in
|
||||
the inventory.
|
||||
"""
|
||||
for i in range(len(inventory)):
|
||||
if isinstance(inventory[i], dict):
|
||||
inventory[i] = self.dict_to_item(inventory[i])
|
||||
return inventory
|
||||
|
||||
def dict_to_item(self, item_dict: dict) -> Entity:
|
||||
"""
|
||||
Translates a dictionnary that contains the state of an item
|
||||
into an item object.
|
||||
"""
|
||||
entity_classes = self.get_all_entity_classes_in_a_dict()
|
||||
|
||||
item_class = entity_classes[item_dict["type"]]
|
||||
item = item_class(**item_dict)
|
||||
item.held_by = self
|
||||
return item
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
The inventory of the merchant is saved in a JSON format.
|
||||
"""
|
||||
d = super().save_state()
|
||||
d["hazel"] = self.hazel
|
||||
d["inventory"] = [item.save_state() for item in self.inventory]
|
||||
return d
|
||||
|
||||
def add_to_inventory(self, obj: Any) -> None:
|
||||
"""
|
||||
Adds an object to the inventory.
|
||||
"""
|
||||
if obj not in self.inventory:
|
||||
obj.held_by = self
|
||||
self.inventory.append(obj)
|
||||
|
||||
def remove_from_inventory(self, obj: Any) -> None:
|
||||
"""
|
||||
Removes an object from the inventory.
|
||||
"""
|
||||
if obj in self.inventory:
|
||||
self.inventory.remove(obj)
|
||||
|
||||
def change_hazel_balance(self, hz: int) -> None:
|
||||
"""
|
||||
Changes the number of hazel the entity has by hz. hz is negative
|
||||
when the entity loses money and positive when it gains money.
|
||||
"""
|
||||
self.hazel += hz
|
||||
|
428
squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
Normal file
428
squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
Normal file
@ -0,0 +1,428 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
|
||||
# This file is distributed under the same license as the squirrelbattle package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: squirrelbattle 23.14\n"
|
||||
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
|
||||
"POT-Creation-Date: 2021-01-10 21:30+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:150
|
||||
msgid "Inventory:"
|
||||
msgstr "Bestand:"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:167
|
||||
msgid "Equipped main:"
|
||||
msgstr "Hauptausgestattete Ding"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:171
|
||||
msgid "Equipped secondary:"
|
||||
msgstr "zusätzlich Ausgestattete Ding"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:176
|
||||
msgid "Equipped chestplate:"
|
||||
msgstr "Ausgestattet Harnisch"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:180
|
||||
msgid "Equipped helmet:"
|
||||
msgstr "Ausgestattet Helm"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:187
|
||||
msgid "YOU ARE DEAD"
|
||||
msgstr "SIE WURDEN GESTORBEN"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:191
|
||||
#, python-brace-format
|
||||
msgid "Use {key} to use the ladder"
|
||||
msgstr "Nutzen {key} um die Leiter zu nutzen"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:210
|
||||
msgid "Move to the friendly entity to talk to it"
|
||||
msgstr "Ziehen Sie zu der freundlichen Einheit hin, um mit ihr zu sprechen"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:212
|
||||
#, python-brace-format
|
||||
msgid "Use {key} then move to talk to the entity"
|
||||
msgstr ""
|
||||
"Verwenden Sie {key} dann bewegen Sie sich, um mit der Einheit zu sprechen"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:124
|
||||
#: squirrelbattle/display/menudisplay.py:149
|
||||
#: squirrelbattle/display/menudisplay.py:304
|
||||
msgid "Credits"
|
||||
msgstr "Abspann"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:173
|
||||
msgid "INVENTORY"
|
||||
msgstr "BESTAND"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:219
|
||||
msgid "STALL"
|
||||
msgstr "STAND"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:263
|
||||
msgid "CHEST"
|
||||
msgstr "KASTE"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:308
|
||||
msgid "Developers:"
|
||||
msgstr "Entwickler:"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:314
|
||||
msgid "Translators:"
|
||||
msgstr "Ubersetzer:"
|
||||
|
||||
#. TODO
|
||||
#: squirrelbattle/entities/friendly.py:38
|
||||
msgid "I don't sell any squirrel"
|
||||
msgstr "Ich verkaufe keinen Eichhörnchen."
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:68
|
||||
msgid "You have opened the chest"
|
||||
msgstr "Sie haben der Kaste geöffnet"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:77
|
||||
msgid "The chest exploded"
|
||||
msgstr "Der Kaste explodierte"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:78
|
||||
msgid "It's not really effective"
|
||||
msgstr "Es ist nicht wirklich effektiv"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "Flower power!!"
|
||||
msgstr "Blumenmacht!!"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "The sun is warm today"
|
||||
msgstr "Die Sonne ist warm heute"
|
||||
|
||||
#. The bomb is exploding.
|
||||
#. Each entity that is close to the bomb takes damages.
|
||||
#. The player earn XP if the entity was killed.
|
||||
#: squirrelbattle/entities/items.py:189
|
||||
msgid "Bomb is exploding."
|
||||
msgstr "Die Bombe explodiert."
|
||||
|
||||
#: squirrelbattle/entities/items.py:385
|
||||
#, python-brace-format
|
||||
msgid "{player} exchanged its body with {entity}."
|
||||
msgstr "{player} täuscht seinem Körper mit {entity} aus."
|
||||
|
||||
#: squirrelbattle/entities/items.py:519
|
||||
msgid ""
|
||||
"The ennemies have -{max(1, self.held_by.intelligence // 2)}strength for 3 "
|
||||
"turns"
|
||||
msgstr ""
|
||||
"Die Feinde haben 3 Runden lang - {max(1, self.held_by.intelligence // 2)} "
|
||||
"Stärke"
|
||||
|
||||
#: squirrelbattle/entities/items.py:552
|
||||
#, python-brace-format
|
||||
msgid "{name}"
|
||||
msgstr "{name}"
|
||||
|
||||
#: squirrelbattle/entities/items.py:600
|
||||
msgid " is shot by an arrow."
|
||||
msgstr " wird von einem Pfeil erschossen."
|
||||
|
||||
#: squirrelbattle/entities/items.py:622
|
||||
msgid " is shot by a fire ball."
|
||||
msgstr " wird von eine Feuerball erschossen."
|
||||
|
||||
#: squirrelbattle/entities/player.py:83
|
||||
msgid "It worked! Nearby ennemies will be confused for 3 turns."
|
||||
msgstr ""
|
||||
"Es funktionierte! In der Nähe befindliche Feinde werden 3 Runden lang "
|
||||
"verwirrt."
|
||||
|
||||
#: squirrelbattle/entities/player.py:86
|
||||
msgid "It worked, but there is no one nearby..."
|
||||
msgstr "Es hat funktioniert, aber es ist niemand in der Nähe ..."
|
||||
|
||||
#: squirrelbattle/entities/player.py:89
|
||||
msgid "The dance was not effective..."
|
||||
msgstr "Der Tanz war nicht effektiv ..."
|
||||
|
||||
#: squirrelbattle/game.py:214
|
||||
#, python-brace-format
|
||||
msgid "The player climbs down to the floor {floor}."
|
||||
msgstr "Der Spieler klettert auf dem Stock {floor} hinunter."
|
||||
|
||||
#: squirrelbattle/game.py:227
|
||||
#, python-brace-format
|
||||
msgid "The player climbs up the floor {floor}."
|
||||
msgstr "Der Spieler klettert auf dem Stock {floor} hinoben."
|
||||
|
||||
#: squirrelbattle/game.py:348 squirrelbattle/tests/game_test.py:631
|
||||
msgid "The buyer does not have enough money"
|
||||
msgstr "Der Kaufer hat nicht genug Geld"
|
||||
|
||||
#: squirrelbattle/game.py:423
|
||||
msgid ""
|
||||
"Some keys are missing in your save file.\n"
|
||||
"Your save seems to be corrupt. It got deleted."
|
||||
msgstr ""
|
||||
"In Ihrer Speicherdatei fehlen einige Schlüssel.\n"
|
||||
"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht."
|
||||
|
||||
#: squirrelbattle/game.py:431
|
||||
msgid ""
|
||||
"No player was found on this map!\n"
|
||||
"Maybe you died?"
|
||||
msgstr ""
|
||||
"Auf dieser Karte wurde kein Spieler gefunden!\n"
|
||||
"Vielleicht sind Sie gestorben?"
|
||||
|
||||
#: squirrelbattle/game.py:454
|
||||
msgid ""
|
||||
"The JSON file is not correct.\n"
|
||||
"Your save seems corrupted. It got deleted."
|
||||
msgstr ""
|
||||
"Die JSON-Datei ist nicht korrekt.\n"
|
||||
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."
|
||||
|
||||
#: squirrelbattle/interfaces.py:758 squirrelbattle/tests/game_test.py:264
|
||||
#, python-brace-format
|
||||
msgid "{name} is confused, it can not hit {opponent}."
|
||||
msgstr "{name} ist verwirrt, es kann {opponent} nicht schlagen."
|
||||
|
||||
#: squirrelbattle/interfaces.py:766
|
||||
msgid "It's a critical hit!"
|
||||
msgstr "Es ist ein kritischer Treffer!"
|
||||
|
||||
#: squirrelbattle/interfaces.py:767
|
||||
#, python-brace-format
|
||||
msgid "{name} hits {opponent}."
|
||||
msgstr "{name} schlägt {opponent}."
|
||||
|
||||
#: squirrelbattle/interfaces.py:782
|
||||
#, python-brace-format
|
||||
msgid "{name} takes {damage} damage."
|
||||
msgstr "{name} erleidet {damage} Schaden."
|
||||
|
||||
#: squirrelbattle/interfaces.py:784
|
||||
#, python-brace-format
|
||||
msgid "{name} dies."
|
||||
msgstr "{name} stirbt."
|
||||
|
||||
#: squirrelbattle/interfaces.py:818
|
||||
#, python-brace-format
|
||||
msgid "{entity} said: {message}"
|
||||
msgstr "{entity} hat gesagt: {message}"
|
||||
|
||||
#: squirrelbattle/menus.py:73
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: squirrelbattle/tests/game_test.py:395 squirrelbattle/tests/game_test.py:398
|
||||
#: squirrelbattle/tests/game_test.py:401 squirrelbattle/tests/game_test.py:404
|
||||
#: squirrelbattle/tests/translations_test.py:16
|
||||
msgid "New game"
|
||||
msgstr "Neu Spiel"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:17
|
||||
msgid "Resume"
|
||||
msgstr "Weitergehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:18
|
||||
msgid "Load"
|
||||
msgstr "Laden"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:19
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:20
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:21
|
||||
msgid "Exit"
|
||||
msgstr "Verlassen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:27
|
||||
msgid "Main key to move up"
|
||||
msgstr "Haupttaste zum Obengehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:29
|
||||
msgid "Secondary key to move up"
|
||||
msgstr "Sekundärtaste zum Obengehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:31
|
||||
msgid "Main key to move down"
|
||||
msgstr "Haupttaste zum Untergehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:33
|
||||
msgid "Secondary key to move down"
|
||||
msgstr "Sekundärtaste zum Untergehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:35
|
||||
msgid "Main key to move left"
|
||||
msgstr "Haupttaste zum Linksgehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:37
|
||||
msgid "Secondary key to move left"
|
||||
msgstr "Sekundärtaste zum Linksgehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:39
|
||||
msgid "Main key to move right"
|
||||
msgstr "Haupttaste zum Rechtsgehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:41
|
||||
msgid "Secondary key to move right"
|
||||
msgstr "Sekundärtaste zum Rechtsgehen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:43
|
||||
msgid "Key to validate a menu"
|
||||
msgstr "Menütaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:45
|
||||
msgid "Key used to open the inventory"
|
||||
msgstr "Bestandtaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:47
|
||||
msgid "Key used to use an item in the inventory"
|
||||
msgstr "Taste um eines Objekts im Bestand zu verwenden"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:49
|
||||
msgid "Key used to equip an item in the inventory"
|
||||
msgstr "Taste um eines Objekts im Bestand auszurüsten"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:51
|
||||
msgid "Key used to drop an item in the inventory"
|
||||
msgstr "Taste um eines Objekts im Bestand zu werfen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:53
|
||||
msgid "Key used to talk to a friendly entity"
|
||||
msgstr "Taste um mit einer friedlicher Entität zu sprechen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:55
|
||||
msgid "Key used to wait"
|
||||
msgstr "Wartentaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:56
|
||||
msgid "Key used to use ladders"
|
||||
msgstr "Leitertaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:58
|
||||
msgid "Key used to use a bow"
|
||||
msgstr "Bogentaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:60
|
||||
msgid "Key used to dance"
|
||||
msgstr "Tanztaste"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:62
|
||||
msgid "Texture pack"
|
||||
msgstr "Textur-Packung"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:63
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:66
|
||||
msgid "player"
|
||||
msgstr "Spieler"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:68
|
||||
msgid "hedgehog"
|
||||
msgstr "Igel"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:69
|
||||
msgid "merchant"
|
||||
msgstr "Kaufmann"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:70
|
||||
msgid "rabbit"
|
||||
msgstr "Kanninchen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:71
|
||||
msgid "sunflower"
|
||||
msgstr "Sonnenblume"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:72
|
||||
msgid "teddy bear"
|
||||
msgstr "Teddybär"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:73
|
||||
msgid "tiger"
|
||||
msgstr "Tiger"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:74
|
||||
msgid "eagle"
|
||||
msgstr "Adler"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:76
|
||||
msgid "body snatch potion"
|
||||
msgstr "Leichenfleddererzaubertrank"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:77
|
||||
msgid "bomb"
|
||||
msgstr "Bombe"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:78
|
||||
msgid "explosion"
|
||||
msgstr "Explosion"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:79
|
||||
msgid "heart"
|
||||
msgstr "Herz"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:80
|
||||
msgid "sword"
|
||||
msgstr "schwert"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:81
|
||||
msgid "helmet"
|
||||
msgstr "Helm"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:82
|
||||
msgid "chestplate"
|
||||
msgstr "Brustpanzer"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:83
|
||||
msgid "shield"
|
||||
msgstr "Schild"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:84
|
||||
msgid "ruler"
|
||||
msgstr "Lineal"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:85
|
||||
msgid "scroll of damage"
|
||||
msgstr "Schriftrolle des Schadens"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:86
|
||||
msgid "scroll of weakness"
|
||||
msgstr "Schriftrolle der Schwäche"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:87
|
||||
msgid "bow"
|
||||
msgstr "Bogen"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:88
|
||||
msgid "fire ball staff"
|
||||
msgstr "Feuerball Stab"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:89
|
||||
msgid "ring of critical damage"
|
||||
msgstr "Ring des kritischen Schadens"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:91
|
||||
msgid "ring of more experience"
|
||||
msgstr "Ring der mehr Erfahrung"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:93
|
||||
msgid "monocle"
|
||||
msgstr "Monokel"
|
424
squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
Normal file
424
squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
Normal file
@ -0,0 +1,424 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
|
||||
# This file is distributed under the same license as the squirrelbattle package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: squirrelbattle 23.14\n"
|
||||
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
|
||||
"POT-Creation-Date: 2021-01-10 21:30+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:150
|
||||
msgid "Inventory:"
|
||||
msgstr "Inventorio :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:167
|
||||
msgid "Equipped main:"
|
||||
msgstr "Equipado principal :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:171
|
||||
msgid "Equipped secondary:"
|
||||
msgstr "Equipado segundario :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:176
|
||||
msgid "Equipped chestplate:"
|
||||
msgstr "Pechera equipada :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:180
|
||||
msgid "Equipped helmet:"
|
||||
msgstr "Casco equipado :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:187
|
||||
msgid "YOU ARE DEAD"
|
||||
msgstr "ERES MUERTO"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:191
|
||||
#, python-brace-format
|
||||
msgid "Use {key} to use the ladder"
|
||||
msgstr "Presiona {key} para utilizar la escala"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:210
|
||||
msgid "Move to the friendly entity to talk to it"
|
||||
msgstr "Moverse hasta la entitad amistosa para hablar con ella"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:212
|
||||
#, python-brace-format
|
||||
msgid "Use {key} then move to talk to the entity"
|
||||
msgstr "Presionar {key} pues moverse para hablar con la entitad"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:124
|
||||
#: squirrelbattle/display/menudisplay.py:149
|
||||
#: squirrelbattle/display/menudisplay.py:304
|
||||
msgid "Credits"
|
||||
msgstr "Créditos"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:173
|
||||
msgid "INVENTORY"
|
||||
msgstr "INVENTORIO"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:219
|
||||
msgid "STALL"
|
||||
msgstr "PUESTO"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:263
|
||||
msgid "CHEST"
|
||||
msgstr "COFRE"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:308
|
||||
msgid "Developers:"
|
||||
msgstr "Desarrolladores :"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:314
|
||||
msgid "Translators:"
|
||||
msgstr "Traductores :"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:38
|
||||
msgid "I don't sell any squirrel"
|
||||
msgstr "No vendo ninguna ardilla"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:68
|
||||
msgid "You have opened the chest"
|
||||
msgstr "Abriste el cofre"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:77
|
||||
msgid "The chest exploded"
|
||||
msgstr "El cofre explotó"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:78
|
||||
msgid "It's not really effective"
|
||||
msgstr "No es realmente efectivo"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "Flower power!!"
|
||||
msgstr "Poder de las flores!!"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "The sun is warm today"
|
||||
msgstr "El sol está caliente hoy"
|
||||
|
||||
#. The bomb is exploding.
|
||||
#. Each entity that is close to the bomb takes damages.
|
||||
#. The player earn XP if the entity was killed.
|
||||
#: squirrelbattle/entities/items.py:189
|
||||
msgid "Bomb is exploding."
|
||||
msgstr "La bomba está explotando."
|
||||
|
||||
#: squirrelbattle/entities/items.py:385
|
||||
#, python-brace-format
|
||||
msgid "{player} exchanged its body with {entity}."
|
||||
msgstr "{player} intercambió su cuerpo con {entity}."
|
||||
|
||||
#: squirrelbattle/entities/items.py:519
|
||||
msgid ""
|
||||
"The ennemies have -{max(1, self.held_by.intelligence // 2)}strength for 3 "
|
||||
"turns"
|
||||
msgstr ""
|
||||
"Los enemigos tienen - {max(1, self.held_by.intelligence // 2)} fuerza "
|
||||
"durante 3turnos"
|
||||
|
||||
#: squirrelbattle/entities/items.py:552
|
||||
#, python-brace-format
|
||||
msgid "{name}"
|
||||
msgstr "{name}"
|
||||
|
||||
#: squirrelbattle/entities/items.py:600
|
||||
msgid " is shot by an arrow."
|
||||
msgstr " es disparado por una flecha."
|
||||
|
||||
#: squirrelbattle/entities/items.py:622
|
||||
msgid " is shot by a fire ball."
|
||||
msgstr " es disparado por una bola de fuego."
|
||||
|
||||
#: squirrelbattle/entities/player.py:83
|
||||
msgid "It worked! Nearby ennemies will be confused for 3 turns."
|
||||
msgstr "¡Funcionó! Los enemigos cercanos se confundirán durante 3 turnos."
|
||||
|
||||
#: squirrelbattle/entities/player.py:86
|
||||
msgid "It worked, but there is no one nearby..."
|
||||
msgstr "Funcionó, pero no hay nadie cerca ..."
|
||||
|
||||
#: squirrelbattle/entities/player.py:89
|
||||
msgid "The dance was not effective..."
|
||||
msgstr "El baile no fue efectivo ..."
|
||||
|
||||
#: squirrelbattle/game.py:214
|
||||
#, python-brace-format
|
||||
msgid "The player climbs down to the floor {floor}."
|
||||
msgstr "El jugador baja a la planta {floor}."
|
||||
|
||||
#: squirrelbattle/game.py:227
|
||||
#, python-brace-format
|
||||
msgid "The player climbs up the floor {floor}."
|
||||
msgstr "El jugador sube a la planta {floor}."
|
||||
|
||||
#: squirrelbattle/game.py:348 squirrelbattle/tests/game_test.py:631
|
||||
msgid "The buyer does not have enough money"
|
||||
msgstr "El comprador no tiene suficiente dinero"
|
||||
|
||||
#: squirrelbattle/game.py:423
|
||||
msgid ""
|
||||
"Some keys are missing in your save file.\n"
|
||||
"Your save seems to be corrupt. It got deleted."
|
||||
msgstr ""
|
||||
"Algunas claves faltan en su archivo de guarda.\n"
|
||||
"Su guarda parece a ser corruptido. Fue eliminado."
|
||||
|
||||
#: squirrelbattle/game.py:431
|
||||
msgid ""
|
||||
"No player was found on this map!\n"
|
||||
"Maybe you died?"
|
||||
msgstr ""
|
||||
"No jugador encontrado sobre la carta !\n"
|
||||
"¿ Quizas murió ?"
|
||||
|
||||
#: squirrelbattle/game.py:454
|
||||
msgid ""
|
||||
"The JSON file is not correct.\n"
|
||||
"Your save seems corrupted. It got deleted."
|
||||
msgstr ""
|
||||
"El JSON archivo no es correcto.\n"
|
||||
"Su guarda parece corrupta. Fue eliminada."
|
||||
|
||||
#: squirrelbattle/interfaces.py:758 squirrelbattle/tests/game_test.py:264
|
||||
#, python-brace-format
|
||||
msgid "{name} is confused, it can not hit {opponent}."
|
||||
msgstr "{name} está confundido, no puede golpear a {opponent}."
|
||||
|
||||
#: squirrelbattle/interfaces.py:766
|
||||
msgid "It's a critical hit!"
|
||||
msgstr "¡Es un golpe crítico!"
|
||||
|
||||
#: squirrelbattle/interfaces.py:767
|
||||
#, python-brace-format
|
||||
msgid "{name} hits {opponent}."
|
||||
msgstr "{name} golpea a {opponent}."
|
||||
|
||||
#: squirrelbattle/interfaces.py:782
|
||||
#, python-brace-format
|
||||
msgid "{name} takes {damage} damage."
|
||||
msgstr "{name} recibe {damage} daño."
|
||||
|
||||
#: squirrelbattle/interfaces.py:784
|
||||
#, python-brace-format
|
||||
msgid "{name} dies."
|
||||
msgstr "{name} se muere."
|
||||
|
||||
#: squirrelbattle/interfaces.py:818
|
||||
#, python-brace-format
|
||||
msgid "{entity} said: {message}"
|
||||
msgstr "{entity} dijo : {message}"
|
||||
|
||||
#: squirrelbattle/menus.py:73
|
||||
msgid "Back"
|
||||
msgstr "Volver"
|
||||
|
||||
#: squirrelbattle/tests/game_test.py:395 squirrelbattle/tests/game_test.py:398
|
||||
#: squirrelbattle/tests/game_test.py:401 squirrelbattle/tests/game_test.py:404
|
||||
#: squirrelbattle/tests/translations_test.py:16
|
||||
msgid "New game"
|
||||
msgstr "Nuevo partido"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:17
|
||||
msgid "Resume"
|
||||
msgstr "Resumir"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:18
|
||||
msgid "Load"
|
||||
msgstr "Cargar"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:19
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:20
|
||||
msgid "Settings"
|
||||
msgstr "Parametros"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:21
|
||||
msgid "Exit"
|
||||
msgstr "Salir"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:27
|
||||
msgid "Main key to move up"
|
||||
msgstr "Primera tecla para subir"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:29
|
||||
msgid "Secondary key to move up"
|
||||
msgstr "Segunda tecla para subir"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:31
|
||||
msgid "Main key to move down"
|
||||
msgstr "Primera tecla para bajar"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:33
|
||||
msgid "Secondary key to move down"
|
||||
msgstr "Segunda tecla para bajar"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:35
|
||||
msgid "Main key to move left"
|
||||
msgstr "Primera tecla para moverse a la izquierda"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:37
|
||||
msgid "Secondary key to move left"
|
||||
msgstr "Segunda tecla para moverse a la izquierda"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:39
|
||||
msgid "Main key to move right"
|
||||
msgstr "Primera tecla para moverse a la derecha"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:41
|
||||
msgid "Secondary key to move right"
|
||||
msgstr "Segunda tecla para moverse a la derecha"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:43
|
||||
msgid "Key to validate a menu"
|
||||
msgstr "Tecla para validar un menú"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:45
|
||||
msgid "Key used to open the inventory"
|
||||
msgstr "Tecla para abrir el inventorio"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:47
|
||||
msgid "Key used to use an item in the inventory"
|
||||
msgstr "Tecla para utilizar un objeto del inventorio"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:49
|
||||
msgid "Key used to equip an item in the inventory"
|
||||
msgstr "Tecla para equipar un objeto del inventorio"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:51
|
||||
msgid "Key used to drop an item in the inventory"
|
||||
msgstr "Tecla para dejar un objeto del inventorio"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:53
|
||||
msgid "Key used to talk to a friendly entity"
|
||||
msgstr "Tecla para hablar con una entidad amiga"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:55
|
||||
msgid "Key used to wait"
|
||||
msgstr "Tecla para espera"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:56
|
||||
msgid "Key used to use ladders"
|
||||
msgstr "Tecla para el uso de las escaleras"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:58
|
||||
msgid "Key used to use a bow"
|
||||
msgstr "Tecla para usar un arco"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:60
|
||||
msgid "Key used to dance"
|
||||
msgstr "Tecla para bailar"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:62
|
||||
msgid "Texture pack"
|
||||
msgstr "Paquete de texturas"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:63
|
||||
msgid "Language"
|
||||
msgstr "Languaje"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:66
|
||||
msgid "player"
|
||||
msgstr "jugador"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:68
|
||||
msgid "hedgehog"
|
||||
msgstr "erizo"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:69
|
||||
msgid "merchant"
|
||||
msgstr "comerciante"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:70
|
||||
msgid "rabbit"
|
||||
msgstr "conejo"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:71
|
||||
msgid "sunflower"
|
||||
msgstr "girasol"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:72
|
||||
msgid "teddy bear"
|
||||
msgstr "osito de peluche"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:73
|
||||
msgid "tiger"
|
||||
msgstr "tigre"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:74
|
||||
msgid "eagle"
|
||||
msgstr "águila"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:76
|
||||
msgid "body snatch potion"
|
||||
msgstr "poción de intercambio"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:77
|
||||
msgid "bomb"
|
||||
msgstr "bomba"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:78
|
||||
msgid "explosion"
|
||||
msgstr "explosión"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:79
|
||||
msgid "heart"
|
||||
msgstr "corazón"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:80
|
||||
msgid "sword"
|
||||
msgstr "espada"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:81
|
||||
msgid "helmet"
|
||||
msgstr "casco"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:82
|
||||
msgid "chestplate"
|
||||
msgstr "pechera"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:83
|
||||
msgid "shield"
|
||||
msgstr "escudo"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:84
|
||||
msgid "ruler"
|
||||
msgstr "Regla"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:85
|
||||
msgid "scroll of damage"
|
||||
msgstr "rollo de daño"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:86
|
||||
msgid "scroll of weakness"
|
||||
msgstr "rollo de debilidad"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:87
|
||||
msgid "bow"
|
||||
msgstr "arco"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:88
|
||||
msgid "fire ball staff"
|
||||
msgstr "bastón de bola de fuego"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:89
|
||||
msgid "ring of critical damage"
|
||||
msgstr "anillo de daño crítico"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:91
|
||||
msgid "ring of more experience"
|
||||
msgstr "anillo de mejorada experiencia"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:93
|
||||
msgid "monocle"
|
||||
msgstr "monóculo"
|
429
squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
Normal file
429
squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
Normal file
@ -0,0 +1,429 @@
|
||||
#, python-brace-format
|
||||
msgid "{name} takes {amount} damage."
|
||||
msgstr "{name} prend {amount} points de dégât."
|
||||
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
|
||||
# This file is distributed under the same license as the squirrelbattle package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: squirrelbattle 23.14\n"
|
||||
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
|
||||
"POT-Creation-Date: 2021-01-10 21:30+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:150
|
||||
msgid "Inventory:"
|
||||
msgstr "Inventaire :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:167
|
||||
msgid "Equipped main:"
|
||||
msgstr "Équipement principal :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:171
|
||||
msgid "Equipped secondary:"
|
||||
msgstr "Équipement secondaire :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:176
|
||||
msgid "Equipped chestplate:"
|
||||
msgstr "Plastron équipé :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:180
|
||||
msgid "Equipped helmet:"
|
||||
msgstr "Casque équipé :"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:187
|
||||
msgid "YOU ARE DEAD"
|
||||
msgstr "VOUS ÊTES MORT"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:191
|
||||
#, python-brace-format
|
||||
msgid "Use {key} to use the ladder"
|
||||
msgstr "Appuyez sur {key} pour utiliser l'échelle"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:210
|
||||
msgid "Move to the friendly entity to talk to it"
|
||||
msgstr "Avancez vers l'entité pour lui parler"
|
||||
|
||||
#: squirrelbattle/display/gamedisplay.py:212
|
||||
#, python-brace-format
|
||||
msgid "Use {key} then move to talk to the entity"
|
||||
msgstr "Appuyez sur {key} puis déplacez-vous pour parler"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:124
|
||||
#: squirrelbattle/display/menudisplay.py:149
|
||||
#: squirrelbattle/display/menudisplay.py:304
|
||||
msgid "Credits"
|
||||
msgstr "Crédits"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:173
|
||||
msgid "INVENTORY"
|
||||
msgstr "INVENTAIRE"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:219
|
||||
msgid "STALL"
|
||||
msgstr "STAND"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:263
|
||||
msgid "CHEST"
|
||||
msgstr "COFFRE"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:308
|
||||
msgid "Developers:"
|
||||
msgstr "Développeurs:"
|
||||
|
||||
#: squirrelbattle/display/menudisplay.py:314
|
||||
msgid "Translators:"
|
||||
msgstr "Traducteurs:"
|
||||
|
||||
#. TODO
|
||||
#: squirrelbattle/entities/friendly.py:38
|
||||
msgid "I don't sell any squirrel"
|
||||
msgstr "Je ne vends pas d'écureuil"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:68
|
||||
msgid "You have opened the chest"
|
||||
msgstr "Vous avez ouvert le coffre"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:77
|
||||
msgid "The chest exploded"
|
||||
msgstr "Le coffre a explosé"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:78
|
||||
msgid "It's not really effective"
|
||||
msgstr "Ce n'est pas très efficace"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "Flower power!!"
|
||||
msgstr "Pouvoir des fleurs !!"
|
||||
|
||||
#: squirrelbattle/entities/friendly.py:101
|
||||
msgid "The sun is warm today"
|
||||
msgstr "Le soleil est chaud aujourd'hui"
|
||||
|
||||
#. The bomb is exploding.
|
||||
#. Each entity that is close to the bomb takes damages.
|
||||
#. The player earn XP if the entity was killed.
|
||||
#: squirrelbattle/entities/items.py:189
|
||||
msgid "Bomb is exploding."
|
||||
msgstr "La bombe explose."
|
||||
|
||||
#: squirrelbattle/entities/items.py:385
|
||||
#, python-brace-format
|
||||
msgid "{player} exchanged its body with {entity}."
|
||||
msgstr "{player} a échangé son corps avec {entity}."
|
||||
|
||||
#: squirrelbattle/entities/items.py:519
|
||||
msgid ""
|
||||
"The ennemies have -{max(1, self.held_by.intelligence // 2)}strength for 3 "
|
||||
"turns"
|
||||
msgstr ""
|
||||
"Les ennemis ont -{max(1, self.held_by.intelligence // 2)} de force pour 3 "
|
||||
"tours"
|
||||
|
||||
#: squirrelbattle/entities/items.py:552
|
||||
#, python-brace-format
|
||||
msgid "{name}"
|
||||
msgstr "{name}"
|
||||
|
||||
#: squirrelbattle/entities/items.py:600
|
||||
msgid " is shot by an arrow."
|
||||
msgstr " est frappé par une flèche."
|
||||
|
||||
#: squirrelbattle/entities/items.py:622
|
||||
msgid " is shot by a fire ball."
|
||||
msgstr " est frappé par une boule de feu."
|
||||
|
||||
#: squirrelbattle/entities/player.py:83
|
||||
msgid "It worked! Nearby ennemies will be confused for 3 turns."
|
||||
msgstr "Ça a marché ! Les ennemis proches seront confus pendant 3 tours"
|
||||
|
||||
#: squirrelbattle/entities/player.py:86
|
||||
msgid "It worked, but there is no one nearby..."
|
||||
msgstr "Ça a marché, mais il n'y a personne à proximité..."
|
||||
|
||||
#: squirrelbattle/entities/player.py:89
|
||||
msgid "The dance was not effective..."
|
||||
msgstr "La dance n'a pas fonctionné..."
|
||||
|
||||
#: squirrelbattle/game.py:214
|
||||
#, python-brace-format
|
||||
msgid "The player climbs down to the floor {floor}."
|
||||
msgstr "Le joueur descend à l'étage {floor}."
|
||||
|
||||
#: squirrelbattle/game.py:227
|
||||
#, python-brace-format
|
||||
msgid "The player climbs up the floor {floor}."
|
||||
msgstr "Le joueur monte à l'étage {floor}."
|
||||
|
||||
#: squirrelbattle/game.py:348 squirrelbattle/tests/game_test.py:631
|
||||
msgid "The buyer does not have enough money"
|
||||
msgstr "L'acheteur n'a pas assez d'argent"
|
||||
|
||||
#: squirrelbattle/game.py:423
|
||||
msgid ""
|
||||
"Some keys are missing in your save file.\n"
|
||||
"Your save seems to be corrupt. It got deleted."
|
||||
msgstr ""
|
||||
"Certaines clés de votre ficher de sauvegarde sont manquantes.\n"
|
||||
"Votre sauvegarde semble corrompue. Elle a été supprimée."
|
||||
|
||||
#: squirrelbattle/game.py:431
|
||||
msgid ""
|
||||
"No player was found on this map!\n"
|
||||
"Maybe you died?"
|
||||
msgstr ""
|
||||
"Aucun joueur n'a été trouvé sur la carte !\n"
|
||||
"Peut-être êtes-vous mort ?"
|
||||
|
||||
#: squirrelbattle/game.py:454
|
||||
msgid ""
|
||||
"The JSON file is not correct.\n"
|
||||
"Your save seems corrupted. It got deleted."
|
||||
msgstr ""
|
||||
"Le fichier JSON de sauvegarde est incorrect.\n"
|
||||
"Votre sauvegarde semble corrompue. Elle a été supprimée."
|
||||
|
||||
#: squirrelbattle/interfaces.py:758 squirrelbattle/tests/game_test.py:264
|
||||
#, python-brace-format
|
||||
msgid "{name} is confused, it can not hit {opponent}."
|
||||
msgstr "{name} est confus et ne peut pas frapper {opponent}."
|
||||
|
||||
#: squirrelbattle/interfaces.py:766
|
||||
msgid "It's a critical hit!"
|
||||
msgstr "C'est un coup critique !"
|
||||
|
||||
#: squirrelbattle/interfaces.py:767
|
||||
#, python-brace-format
|
||||
msgid "{name} hits {opponent}."
|
||||
msgstr "{name} frappe {opponent}."
|
||||
|
||||
#: squirrelbattle/interfaces.py:782
|
||||
#, python-brace-format
|
||||
msgid "{name} takes {damage} damage."
|
||||
msgstr "{name} prend {damage} dégâts."
|
||||
|
||||
#: squirrelbattle/interfaces.py:784
|
||||
#, python-brace-format
|
||||
msgid "{name} dies."
|
||||
msgstr "{name} meurt."
|
||||
|
||||
#: squirrelbattle/interfaces.py:818
|
||||
#, python-brace-format
|
||||
msgid "{entity} said: {message}"
|
||||
msgstr "{entity} a dit : {message}"
|
||||
|
||||
#: squirrelbattle/menus.py:73
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
|
||||
#: squirrelbattle/tests/game_test.py:395 squirrelbattle/tests/game_test.py:398
|
||||
#: squirrelbattle/tests/game_test.py:401 squirrelbattle/tests/game_test.py:404
|
||||
#: squirrelbattle/tests/translations_test.py:16
|
||||
msgid "New game"
|
||||
msgstr "Nouvelle partie"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:17
|
||||
msgid "Resume"
|
||||
msgstr "Continuer"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:18
|
||||
msgid "Load"
|
||||
msgstr "Charger"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:19
|
||||
msgid "Save"
|
||||
msgstr "Sauvegarder"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:20
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:21
|
||||
msgid "Exit"
|
||||
msgstr "Quitter"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:27
|
||||
msgid "Main key to move up"
|
||||
msgstr "Touche principale pour aller vers le haut"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:29
|
||||
msgid "Secondary key to move up"
|
||||
msgstr "Touche secondaire pour aller vers le haut"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:31
|
||||
msgid "Main key to move down"
|
||||
msgstr "Touche principale pour aller vers le bas"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:33
|
||||
msgid "Secondary key to move down"
|
||||
msgstr "Touche secondaire pour aller vers le bas"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:35
|
||||
msgid "Main key to move left"
|
||||
msgstr "Touche principale pour aller vers la gauche"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:37
|
||||
msgid "Secondary key to move left"
|
||||
msgstr "Touche secondaire pour aller vers la gauche"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:39
|
||||
msgid "Main key to move right"
|
||||
msgstr "Touche principale pour aller vers la droite"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:41
|
||||
msgid "Secondary key to move right"
|
||||
msgstr "Touche secondaire pour aller vers la droite"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:43
|
||||
msgid "Key to validate a menu"
|
||||
msgstr "Touche pour valider un menu"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:45
|
||||
msgid "Key used to open the inventory"
|
||||
msgstr "Touche utilisée pour ouvrir l'inventaire"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:47
|
||||
msgid "Key used to use an item in the inventory"
|
||||
msgstr "Touche pour utiliser un objet de l'inventaire"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:49
|
||||
msgid "Key used to equip an item in the inventory"
|
||||
msgstr "Touche pour équiper un objet de l'inventaire"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:51
|
||||
msgid "Key used to drop an item in the inventory"
|
||||
msgstr "Touche pour jeter un objet de l'inventaire"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:53
|
||||
msgid "Key used to talk to a friendly entity"
|
||||
msgstr "Touche pour parler à une entité pacifique"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:55
|
||||
msgid "Key used to wait"
|
||||
msgstr "Touche pour attendre"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:56
|
||||
msgid "Key used to use ladders"
|
||||
msgstr "Touche pour utiliser les échelles"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:58
|
||||
msgid "Key used to use a bow"
|
||||
msgstr "Touche pour utiliser un arc"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:60
|
||||
msgid "Key used to dance"
|
||||
msgstr "Touche pour danser"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:62
|
||||
msgid "Texture pack"
|
||||
msgstr "Pack de textures"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:63
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:66
|
||||
msgid "player"
|
||||
msgstr "joueur"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:68
|
||||
msgid "hedgehog"
|
||||
msgstr "hérisson"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:69
|
||||
msgid "merchant"
|
||||
msgstr "marchand"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:70
|
||||
msgid "rabbit"
|
||||
msgstr "lapin"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:71
|
||||
msgid "sunflower"
|
||||
msgstr "tournesol"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:72
|
||||
msgid "teddy bear"
|
||||
msgstr "nounours"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:73
|
||||
msgid "tiger"
|
||||
msgstr "tigre"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:74
|
||||
msgid "eagle"
|
||||
msgstr "pygargue"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:76
|
||||
msgid "body snatch potion"
|
||||
msgstr "potion d'arrachage de corps"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:77
|
||||
msgid "bomb"
|
||||
msgstr "bombe"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:78
|
||||
msgid "explosion"
|
||||
msgstr "explosion"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:79
|
||||
msgid "heart"
|
||||
msgstr "cœur"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:80
|
||||
msgid "sword"
|
||||
msgstr "épée"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:81
|
||||
msgid "helmet"
|
||||
msgstr "casque"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:82
|
||||
msgid "chestplate"
|
||||
msgstr "plastron"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:83
|
||||
msgid "shield"
|
||||
msgstr "bouclier"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:84
|
||||
msgid "ruler"
|
||||
msgstr "règle"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:85
|
||||
msgid "scroll of damage"
|
||||
msgstr "parchemin de dégâts"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:86
|
||||
msgid "scroll of weakness"
|
||||
msgstr "parchemin de faiblesse"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:87
|
||||
msgid "bow"
|
||||
msgstr "arc"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:88
|
||||
msgid "fire ball staff"
|
||||
msgstr "baton de boule de feu"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:89
|
||||
msgid "ring of critical damage"
|
||||
msgstr "anneau de coup critique"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:91
|
||||
msgid "ring of more experience"
|
||||
msgstr "anneau de plus d'expérience"
|
||||
|
||||
#: squirrelbattle/tests/translations_test.py:93
|
||||
msgid "monocle"
|
||||
msgstr "monocle"
|
0
squirrelbattle/mapgeneration/__init__.py
Normal file
0
squirrelbattle/mapgeneration/__init__.py
Normal file
480
squirrelbattle/mapgeneration/broguelike.py
Normal file
480
squirrelbattle/mapgeneration/broguelike.py
Normal file
@ -0,0 +1,480 @@
|
||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import choice, choices, randint, random, shuffle
|
||||
from typing import List, Tuple
|
||||
|
||||
from ..interfaces import Entity, Map, Tile
|
||||
|
||||
DEFAULT_PARAMS = {
|
||||
"width": 120,
|
||||
"height": 35,
|
||||
"tries": 300,
|
||||
"max_rooms": 20,
|
||||
"max_room_tries": 15,
|
||||
"cross_room": 1,
|
||||
"corridor_chance": .2,
|
||||
"min_v_corr": 2,
|
||||
"max_v_corr": 6,
|
||||
"min_h_corr": 4,
|
||||
"max_h_corr": 12,
|
||||
"large_circular_room": .10,
|
||||
"circular_holes": .5,
|
||||
"loop_tries": 40,
|
||||
"loop_max": 5,
|
||||
"loop_threshold": 15,
|
||||
"spawn_per_region": [1, 2],
|
||||
"room_chances": {
|
||||
"circular": 5,
|
||||
"chunks": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def dist(level: List[List[Tile]], y1: int, x1: int, y2: int, x2: int) -> int:
|
||||
"""
|
||||
Compute the minimum walking distance between points (y1, x1) and (y2, x2)
|
||||
on a Tile grid
|
||||
"""
|
||||
# simple breadth first search
|
||||
copy = [[t for t in row] for row in level]
|
||||
dist = -1
|
||||
queue, next_queue = [[y1, x1]], [0]
|
||||
while next_queue:
|
||||
next_queue = []
|
||||
dist += 1
|
||||
while queue:
|
||||
y, x = queue.pop()
|
||||
copy[y][x] = Tile.EMPTY
|
||||
if y == y2 and x == x2:
|
||||
return dist
|
||||
for y, x in Map.neighbourhood(copy, y, x):
|
||||
if copy[y][x].can_walk():
|
||||
next_queue.append([y, x])
|
||||
queue = next_queue
|
||||
return -1
|
||||
|
||||
|
||||
class Generator:
|
||||
def __init__(self, params: dict = None):
|
||||
self.params = params or DEFAULT_PARAMS
|
||||
self.spawn_areas = []
|
||||
self.queued_area = None
|
||||
|
||||
@staticmethod
|
||||
def room_fits(level: List[List[Tile]], y: int, x: int,
|
||||
room: List[List[Tile]], door_y: int, door_x: int,
|
||||
dy: int, dx: int) -> bool:
|
||||
"""
|
||||
Using point (door_y, door_x) in the room as a reference and placing it
|
||||
over point (y, x) in the level, returns whether or not the room fits
|
||||
here
|
||||
"""
|
||||
lh, lw = len(level), len(level[0])
|
||||
rh, rw = len(room), len(room[0])
|
||||
if not(0 < y + dy < lh and 0 < x + dx < lw):
|
||||
return False
|
||||
# door must be placed on an empty tile, and point into a floor tile
|
||||
if level[y][x] != Tile.EMPTY or level[y + dy][x + dx] != Tile.FLOOR:
|
||||
return False
|
||||
# now we verify floor tiles in both grids do not overlap
|
||||
for ry in range(rh):
|
||||
for rx in range(rw):
|
||||
if room[ry][rx] == Tile.FLOOR:
|
||||
ly, lx = y + ry - door_y, x + rx - door_x
|
||||
# tile must be in bounds and empty
|
||||
if not(0 <= ly < lh and 0 <= lx < lw) or \
|
||||
level[ly][lx] == Tile.FLOOR:
|
||||
return False
|
||||
# so do all neighbouring tiles bc we may
|
||||
# need to place walls there eventually
|
||||
for ny, nx in Map.neighbourhood(level, ly, lx,
|
||||
large=True, oob=True):
|
||||
if not(0 <= ny < lh and 0 <= nx < lw) or \
|
||||
level[ny][nx] != Tile.EMPTY:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def place_room(level: List[List[Tile]], y: int, x: int,
|
||||
room: List[List[Tile]], door_y: int, door_x: int) -> None:
|
||||
"""
|
||||
Mutates level in place to add the room. Placement is determined by
|
||||
making (door_y, door_x) in the room correspond with (y, x) in the level
|
||||
"""
|
||||
rh, rw = len(room), len(room[0])
|
||||
level[y][x] = Tile.DOOR
|
||||
for ry in range(rh):
|
||||
for rx in range(rw):
|
||||
if room[ry][rx] == Tile.FLOOR:
|
||||
level[y - door_y + ry][x - door_x + rx] = Tile.FLOOR
|
||||
|
||||
@staticmethod
|
||||
def add_loop(level: List[List[Tile]], y: int, x: int) -> bool:
|
||||
"""
|
||||
Try to add a corridor between two far apart floor tiles, passing
|
||||
through point (y, x).
|
||||
"""
|
||||
h, w = len(level), len(level[0])
|
||||
|
||||
if level[y][x] != Tile.EMPTY:
|
||||
return False
|
||||
|
||||
# loop over both directions, trying to place both veritcal
|
||||
# and horizontal corridors
|
||||
for dx, dy in [[0, 1], [1, 0]]:
|
||||
# then we find two floor tiles, one on each side of (y, x)
|
||||
# exiting if we don't find two (reach the edge of the map before)
|
||||
y1, x1, y2, x2 = y, x, y, x
|
||||
while x1 >= 0 and y1 >= 0 and level[y1][x1] == Tile.EMPTY:
|
||||
y1, x1 = y1 - dy, x1 - dx
|
||||
while x2 < w and y2 < h and level[y2][x2] == Tile.EMPTY:
|
||||
y2, x2 = y2 + dy, x2 + dx
|
||||
if not(0 <= x1 <= x2 < w and 0 <= y1 <= y2 < h):
|
||||
continue
|
||||
|
||||
def verify_sides() -> bool:
|
||||
# switching up dy and dx here pivots the axis, so
|
||||
# (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to
|
||||
# (y, x), but not on the original axis
|
||||
for delta_x, delta_y in [[dy, dx], [-dy, -dx]]:
|
||||
for i in range(1, y2 - y1 + x2 - x1):
|
||||
if not (0 <= y1 + delta_y + i * dy < h
|
||||
and 0 <= x1 + delta_x + i * dx < w) or \
|
||||
level[y1 + delta_y + i * dy][x1 + delta_x
|
||||
+ i * dx]\
|
||||
.can_walk():
|
||||
return False
|
||||
return True
|
||||
# if adding the path would make the two tiles significantly closer
|
||||
# and its sides don't touch already placed terrain, build it
|
||||
if dist(level, y1, x1, y2, x2) < 20 and verify_sides():
|
||||
y, x = y1 + dy, x1 + dx
|
||||
while level[y][x] == Tile.EMPTY:
|
||||
level[y][x] = Tile.FLOOR
|
||||
y, x = y + dy, x + dx
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def place_walls(level: List[List[Tile]]) -> None:
|
||||
"""
|
||||
Place wall tiles on every empty tile that is adjacent (in the largest
|
||||
sense), to a floor tile
|
||||
"""
|
||||
h, w = len(level), len(level[0])
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if not level[y][x].is_wall():
|
||||
for ny, nx in Map.neighbourhood(level, y, x, large=True):
|
||||
if level[ny][nx] == Tile.EMPTY:
|
||||
level[ny][nx] = Tile.WALL
|
||||
|
||||
def corr_meta_info(self) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Return info about the extra grid space that should be allocated for the
|
||||
room, and where the room should be built along this extra grid space.
|
||||
Because grids are usually thight around the room, this gives us extra
|
||||
place to add a corridor later. Corridor length and orientation is
|
||||
implicitly derived from this info.
|
||||
|
||||
h_sup and w_sup represent the extra needed space along each axis,
|
||||
and h_off and w_off are the offset at which to build the room
|
||||
"""
|
||||
if random() < self.params["corridor_chance"]:
|
||||
h_sup = randint(self.params["min_v_corr"],
|
||||
self.params["max_v_corr"]) if random() < .5 else 0
|
||||
# we only allow extra space allocation along one axis,
|
||||
# because there won't more than one exit corridor
|
||||
w_sup = 0 if h_sup else randint(self.params["min_h_corr"],
|
||||
self.params["max_h_corr"])
|
||||
# implicitly choose which direction along the axis
|
||||
# the corridor will be pointing to
|
||||
h_off = h_sup if random() < .5 else 0
|
||||
w_off = w_sup if random() < .5 else 0
|
||||
return h_sup, w_sup, h_off, w_off
|
||||
return 0, 0, 0, 0
|
||||
|
||||
@staticmethod
|
||||
def build_door(room: List[List[Tile]], y: int, x: int,
|
||||
dy: int, dx: int, length: int) -> bool:
|
||||
"""
|
||||
Tries to build the exit from the room at given coordinates
|
||||
Depending on parameter length, it will either attempt to build a
|
||||
simple door, or a long corridor. Return value is a boolean
|
||||
signifying whether or not the exit was successfully built
|
||||
"""
|
||||
rh, rw = len(room), len(room[0])
|
||||
# verify we are pointing away from a floor tile
|
||||
if not(0 <= y - dy < rh and 0 <= x - dx < rw) \
|
||||
or room[y - dy][x - dx] != Tile.FLOOR:
|
||||
return False
|
||||
# verify there's no other floor tile around us
|
||||
for ny, nx in [[y + dy, x + dx], [y - dx, x - dy],
|
||||
[y + dx, x + dy]]:
|
||||
if 0 <= ny < rh and 0 <= nx < rw \
|
||||
and room[ny][nx] != Tile.EMPTY:
|
||||
return False
|
||||
# see if the path ahead is clear. needed in the case of non convex room
|
||||
for i in range(length + 1):
|
||||
if room[y + i * dy][x + i * dx] != Tile.EMPTY:
|
||||
return False
|
||||
for i in range(length):
|
||||
room[y + i * dy][x + i * dx] = Tile.FLOOR
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def attach_door(room: List[List[Tile]], h_sup: int, w_sup: int,
|
||||
h_off: int, w_off: int) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Attach an exit to the room. If extra space was allocated to
|
||||
the grid, make sure a corridor is properly built
|
||||
"""
|
||||
length = h_sup + w_sup
|
||||
dy, dx = 0, 0
|
||||
if length > 0:
|
||||
if h_sup:
|
||||
dy = -1 if h_off else 1
|
||||
else:
|
||||
dx = -1 if w_off else 1
|
||||
else:
|
||||
# determine door direction for rooms without corridors
|
||||
if random() < .5:
|
||||
dy = -1 if random() < .5 else 1
|
||||
else:
|
||||
dx = -1 if random() < .5 else 1
|
||||
|
||||
# loop over all possible positions in a random order
|
||||
rh, rw = len(room), len(room[0])
|
||||
yxs = [i for i in range(rh * rw)]
|
||||
shuffle(yxs)
|
||||
for pos in yxs:
|
||||
y, x = pos // rw, pos % rw
|
||||
if room[y][x] == Tile.EMPTY and \
|
||||
Generator.build_door(room, y, x, dy, dx, length):
|
||||
break
|
||||
else: # pragma: no cover
|
||||
return None, None, None, None
|
||||
|
||||
return y + length * dy, x + length * dx, dy, dx
|
||||
|
||||
def create_chunk_room(self, spawnable: bool = True) \
|
||||
-> Tuple[List[List[Tile]], int, int, int, int]:
|
||||
"""
|
||||
Create and return as a tile grid a room that is composed of multiple
|
||||
overlapping circles of the same radius
|
||||
Also return door info so we know how to place the room in the level
|
||||
"""
|
||||
height, width = 15, 15
|
||||
nb_chunks, r = 6, 3
|
||||
|
||||
h_sup, w_sup, h_off, w_off = self.corr_meta_info()
|
||||
room = [[Tile.EMPTY] * (width + w_sup)
|
||||
for _dummy in range(height + h_sup)]
|
||||
|
||||
def draw_chunk(room: List[List[Tile]], y: int, x: int) -> None:
|
||||
for i in range(y - r, y + r + 1):
|
||||
d = (y - i)**2
|
||||
for j in range(x - r, x + r + 1):
|
||||
if d + (x - j) ** 2 < r ** 2:
|
||||
room[i][j] = Tile.FLOOR
|
||||
|
||||
draw_chunk(room, h_off + height // 2 + 1, w_off + width // 2 + 1)
|
||||
|
||||
min_w, max_w = w_off + r + 1, width + w_off - r - 1
|
||||
min_h, max_h = h_off + r + 1, height + h_off - r - 1
|
||||
for i in range(nb_chunks):
|
||||
y, x = randint(min_h, max_h), randint(min_w, max_w)
|
||||
while room[y][x] != Tile.FLOOR:
|
||||
y, x = randint(min_h, max_h), randint(min_w, max_w)
|
||||
draw_chunk(room, y, x)
|
||||
|
||||
# log all placed tiles as spawn positions
|
||||
if spawnable:
|
||||
self.register_spawn_area(room)
|
||||
|
||||
# attach exit
|
||||
door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup,
|
||||
h_off, w_off)
|
||||
|
||||
return room, door_y, door_x, dy, dx
|
||||
|
||||
def create_circular_room(self, spawnable: bool = True) \
|
||||
-> Tuple[List[List[Tile]], int, int, int, int]:
|
||||
"""
|
||||
Create and return as a tile grid a room that is circular in shape, and
|
||||
may have a center, also circular hole
|
||||
Also return door info so we know how to place the room in the level
|
||||
"""
|
||||
if random() < self.params["large_circular_room"]:
|
||||
r = randint(5, 10)
|
||||
else:
|
||||
r = randint(2, 4)
|
||||
|
||||
room = []
|
||||
|
||||
h_sup, w_sup, h_off, w_off = self.corr_meta_info()
|
||||
|
||||
height = 2 * r + 2
|
||||
width = 2 * r + 2
|
||||
make_hole = r > 6 and random() < self.params["circular_holes"]
|
||||
r2 = 0
|
||||
if make_hole:
|
||||
r2 = randint(3, r - 3)
|
||||
for i in range(height + h_sup):
|
||||
room.append([])
|
||||
d = (i - h_off - height // 2) ** 2
|
||||
for j in range(width + w_sup):
|
||||
if d + (j - w_off - width // 2) ** 2 < r ** 2 and \
|
||||
(not make_hole
|
||||
or d + (j - w_off - width // 2) ** 2 >= r2 ** 2):
|
||||
room[-1].append(Tile.FLOOR)
|
||||
else:
|
||||
room[-1].append(Tile.EMPTY)
|
||||
|
||||
# log all placed tiles as spawn positions
|
||||
if spawnable:
|
||||
self.register_spawn_area(room)
|
||||
|
||||
# attach exit
|
||||
door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup,
|
||||
h_off, w_off)
|
||||
|
||||
return room, door_y, door_x, dy, dx
|
||||
|
||||
def create_random_room(self, spawnable: bool = True) \
|
||||
-> Tuple[List[list], int, int, int, int]:
|
||||
"""
|
||||
Randomly select a room shape and return one such room along with its
|
||||
door info. Set spawnable to False is the room should be marked as a
|
||||
potential spawning region on the map
|
||||
"""
|
||||
coef_dict = self.params["room_chances"]
|
||||
sum_coefs = sum(coef_dict[key] for key in coef_dict)
|
||||
target = randint(1, sum_coefs)
|
||||
for key in coef_dict:
|
||||
if target > coef_dict[key]:
|
||||
target -= coef_dict[key]
|
||||
else:
|
||||
break
|
||||
|
||||
if key == "circular":
|
||||
return self.create_circular_room(spawnable=spawnable)
|
||||
elif key == "chunks":
|
||||
return self.create_chunk_room(spawnable=spawnable)
|
||||
|
||||
def register_spawn_area(self, area: List[List[Tile]]) -> None:
|
||||
"""
|
||||
Register all floor positions relative to the input grid
|
||||
for later use
|
||||
"""
|
||||
spawn_positions = []
|
||||
for y, line in enumerate(area):
|
||||
for x, tile in enumerate(line):
|
||||
if tile == Tile.FLOOR:
|
||||
spawn_positions.append([y, x])
|
||||
self.queued_area = spawn_positions
|
||||
|
||||
def update_spawnable(self, y: int, x: int) -> None:
|
||||
"""
|
||||
Convert previous spawn positions relative to the room grid to actual
|
||||
actual spawn positions on the level grid, using the position of the
|
||||
top left corner of the room on the level, then log them as a
|
||||
spawnable region
|
||||
"""
|
||||
if self.queued_area is not None:
|
||||
translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area]
|
||||
self.spawn_areas.append(translated_area)
|
||||
self.queued_area = None
|
||||
|
||||
def populate(self, rv: Map) -> None:
|
||||
"""
|
||||
Populate every spawnable area with some randomly chosen, randomly
|
||||
placed entity
|
||||
"""
|
||||
min_c, max_c = self.params["spawn_per_region"]
|
||||
for region in self.spawn_areas:
|
||||
entity_count = randint(min_c, max_c)
|
||||
for _dummy in range(entity_count):
|
||||
entity = choices(Entity.get_all_entity_classes(),
|
||||
weights=Entity.get_weights(), k=1)[0]()
|
||||
y, x = choice(region)
|
||||
entity.move(y, x)
|
||||
rv.add_entity(entity)
|
||||
|
||||
def run(self) -> Map:
|
||||
"""
|
||||
Using procedural generation, build and return a full map, populated
|
||||
with entities
|
||||
"""
|
||||
height, width = self.params["height"], self.params["width"]
|
||||
level = [width * [Tile.EMPTY] for _ignored in range(height)]
|
||||
|
||||
# the starting room must have no corridor
|
||||
mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0
|
||||
starting_room, _, _, _, _ = self.create_random_room(spawnable=False)
|
||||
dim_v, dim_h = len(starting_room), len(starting_room[0])
|
||||
# because Generator.room_fits checks that the exit door is correctly
|
||||
# placed, but the starting room has no exit door, we find a positoin
|
||||
# manually
|
||||
pos_y, pos_x = randint(0, height - dim_v - 1),\
|
||||
randint(0, width - dim_h - 1)
|
||||
self.place_room(level, pos_y, pos_x, starting_room, 0, 0)
|
||||
# remove the door that was placed
|
||||
if starting_room[0][0] != Tile.FLOOR:
|
||||
level[pos_y][pos_x] = Tile.EMPTY
|
||||
self.params["corridor_chance"] = mem
|
||||
|
||||
# find a starting position for the player
|
||||
sy, sx = randint(0, height - 1), randint(0, width - 1)
|
||||
while level[sy][sx] != Tile.FLOOR:
|
||||
sy, sx = randint(0, height - 1), randint(0, width - 1)
|
||||
level[sy][sx] = Tile.LADDER
|
||||
|
||||
# now we loop until we're bored, or we've added enough rooms
|
||||
tries, rooms_built = 0, 0
|
||||
while tries < self.params["tries"] \
|
||||
and rooms_built < self.params["max_rooms"]:
|
||||
|
||||
# build a room, try to fit it everywhere in a random order, and
|
||||
# place it at the first possible position
|
||||
room, door_y, door_x, dy, dx = self.create_random_room()
|
||||
positions = [i for i in range(height * width)]
|
||||
shuffle(positions)
|
||||
for pos in positions:
|
||||
y, x = pos // width, pos % width
|
||||
if self.room_fits(level, y, x, room, door_y, door_x, dy, dx):
|
||||
self.update_spawnable(y - door_y, x - door_x)
|
||||
self.place_room(level, y, x, room, door_y, door_x)
|
||||
rooms_built += 1
|
||||
break
|
||||
tries += 1
|
||||
|
||||
# post-processing
|
||||
self.place_walls(level)
|
||||
|
||||
# because when a room is placed, it leads to exactly one previously
|
||||
# placed room, the level has a tree like structure with the starting
|
||||
# room as the root
|
||||
# to avoid boring player backtracking, we add some cycles to the room
|
||||
# graph in post processing by placing additional corridors
|
||||
tries, loops = 0, 0
|
||||
while tries < self.params["loop_tries"] and \
|
||||
loops < self.params["loop_max"]:
|
||||
tries += 1
|
||||
y, x = randint(0, height - 1), randint(0, width - 1)
|
||||
loops += self.add_loop(level, y, x)
|
||||
|
||||
# place an exit ladder
|
||||
y, x = randint(0, height - 1), randint(0, width - 1)
|
||||
while level[y][x] != Tile.FLOOR or \
|
||||
any([level[j][i].is_wall() for j, i
|
||||
in Map.neighbourhood(level, y, x, large=True)]):
|
||||
y, x = randint(0, height - 1), randint(0, width - 1)
|
||||
level[y][x] = Tile.LADDER
|
||||
|
||||
# spawn entities
|
||||
rv = Map(width, height, level, sy, sx)
|
||||
self.populate(rv)
|
||||
|
||||
return rv
|
@ -1,14 +1,20 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
|
||||
from .display.texturepack import TexturePack
|
||||
from .enums import GameMode, KeyValues, DisplayActions
|
||||
from .entities.friendly import Chest, Merchant
|
||||
from .entities.player import Player
|
||||
from .enums import DisplayActions, GameMode, KeyValues
|
||||
from .settings import Settings
|
||||
from .translations import gettext as _, Translator
|
||||
|
||||
|
||||
class Menu:
|
||||
"""
|
||||
A Menu object is the logical representation of a menu in the game
|
||||
A Menu object is the logical representation of a menu in the game.
|
||||
"""
|
||||
values: list
|
||||
|
||||
@ -17,59 +23,59 @@ class Menu:
|
||||
|
||||
def go_up(self) -> None:
|
||||
"""
|
||||
Moves the pointer of the menu on the previous value
|
||||
Moves the pointer of the menu on the previous value.
|
||||
"""
|
||||
self.position = max(0, self.position - 1)
|
||||
|
||||
def go_down(self) -> None:
|
||||
"""
|
||||
Moves the pointer of the menu on the next value
|
||||
Moves the pointer of the menu on the next value.
|
||||
"""
|
||||
self.position = min(len(self.values) - 1, self.position + 1)
|
||||
|
||||
def validate(self) -> Any:
|
||||
"""
|
||||
Selects the value that is pointed by the menu pointer
|
||||
Selects the value that is pointed by the menu pointer.
|
||||
"""
|
||||
return self.values[self.position]
|
||||
|
||||
|
||||
class MainMenuValues(Enum):
|
||||
"""
|
||||
Values of the main menu
|
||||
Values of the main menu.
|
||||
"""
|
||||
START = 'Nouvelle partie'
|
||||
RESUME = 'Continuer'
|
||||
SAVE = 'Sauvegarder'
|
||||
LOAD = 'Charger'
|
||||
SETTINGS = 'Paramètres'
|
||||
EXIT = 'Quitter'
|
||||
START = "New game"
|
||||
RESUME = "Resume"
|
||||
SAVE = "Save"
|
||||
LOAD = "Load"
|
||||
SETTINGS = "Settings"
|
||||
EXIT = "Exit"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
return _(self.value)
|
||||
|
||||
|
||||
class MainMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the main menu
|
||||
A special instance of a menu : the main menu.
|
||||
"""
|
||||
values = [e for e in MainMenuValues]
|
||||
|
||||
|
||||
class SettingsMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the settings menu
|
||||
A special instance of a menu : the settings menu.
|
||||
"""
|
||||
waiting_for_key: bool = False
|
||||
|
||||
def update_values(self, settings: Settings) -> None:
|
||||
self.values = list(settings.__dict__.items())
|
||||
self.values.append(("RETURN", ["", "Retour"]))
|
||||
self.values.append(("RETURN", ["", _("Back")]))
|
||||
|
||||
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
|
||||
game: Any) -> None:
|
||||
"""
|
||||
In the setting menu, we van select a setting and change it
|
||||
In the setting menu, we can select a setting and change it.
|
||||
"""
|
||||
if not self.waiting_for_key:
|
||||
# Navigate normally through the menu.
|
||||
@ -92,6 +98,13 @@ class SettingsMenu(Menu):
|
||||
game.settings.TEXTURE_PACK)
|
||||
game.settings.write_settings()
|
||||
self.update_values(game.settings)
|
||||
elif option == "LOCALE":
|
||||
game.settings.LOCALE = 'fr' if game.settings.LOCALE == 'en'\
|
||||
else 'de' if game.settings.LOCALE == 'fr' else 'es' \
|
||||
if game.settings.LOCALE == 'de' else 'en'
|
||||
Translator.setlocale(game.settings.LOCALE)
|
||||
game.settings.write_settings()
|
||||
self.update_values(game.settings)
|
||||
else:
|
||||
self.waiting_for_key = True
|
||||
self.update_values(game.settings)
|
||||
@ -105,3 +118,63 @@ class SettingsMenu(Menu):
|
||||
game.settings.write_settings()
|
||||
self.waiting_for_key = False
|
||||
self.update_values(game.settings)
|
||||
|
||||
|
||||
class InventoryMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the menu for the inventory of the player.
|
||||
"""
|
||||
player: Player
|
||||
|
||||
def update_player(self, player: Player) -> None:
|
||||
"""
|
||||
Updates the player.
|
||||
"""
|
||||
self.player = player
|
||||
|
||||
@property
|
||||
def values(self) -> list:
|
||||
"""
|
||||
Returns the values of the menu.
|
||||
"""
|
||||
return self.player.inventory
|
||||
|
||||
|
||||
class StoreMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the menu for the inventory of a merchant.
|
||||
"""
|
||||
merchant: Merchant = None
|
||||
|
||||
def update_merchant(self, merchant: Merchant) -> None:
|
||||
"""
|
||||
Updates the merchant.
|
||||
"""
|
||||
self.merchant = merchant
|
||||
|
||||
@property
|
||||
def values(self) -> list:
|
||||
"""
|
||||
Returns the values of the menu.
|
||||
"""
|
||||
return self.merchant.inventory if self.merchant else []
|
||||
|
||||
|
||||
class ChestMenu(Menu):
|
||||
"""
|
||||
A special instance of a menu : the menu for the inventory of a chest.
|
||||
"""
|
||||
chest: Chest = None
|
||||
|
||||
def update_chest(self, chest: Chest) -> None:
|
||||
"""
|
||||
Updates the player.
|
||||
"""
|
||||
self.chest = chest
|
||||
|
||||
@property
|
||||
def values(self) -> list:
|
||||
"""
|
||||
Returns the values of the menu.
|
||||
"""
|
||||
return self.chest.inventory if self.chest else []
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
@ -1,37 +1,44 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
from typing import Any, Generator
|
||||
|
||||
from .resources import ResourceManager
|
||||
from .translations import gettext as _
|
||||
|
||||
|
||||
class Settings:
|
||||
"""
|
||||
This class stores the settings of the game.
|
||||
Settings can be get by using for example settings.TEXTURE_PACK directly.
|
||||
The comment can be get by using settings.get_comment('TEXTURE_PACK').
|
||||
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
|
||||
Settings can be obtained by using for example settings.TEXTURE_PACK
|
||||
directly.
|
||||
The comment can be obtained by using settings.get_comment('TEXTURE_PACK').
|
||||
We can set the setting by simply using settings.TEXTURE_PACK = 'new_key'
|
||||
"""
|
||||
def __init__(self):
|
||||
self.KEY_UP_PRIMARY = \
|
||||
['z', 'Touche principale pour aller vers le haut']
|
||||
self.KEY_UP_SECONDARY = \
|
||||
['KEY_UP', 'Touche secondaire pour aller vers le haut']
|
||||
self.KEY_DOWN_PRIMARY = \
|
||||
['s', 'Touche principale pour aller vers le bas']
|
||||
self.KEY_DOWN_SECONDARY = \
|
||||
['KEY_DOWN', 'Touche secondaire pour aller vers le bas']
|
||||
self.KEY_LEFT_PRIMARY = \
|
||||
['q', 'Touche principale pour aller vers la gauche']
|
||||
self.KEY_LEFT_SECONDARY = \
|
||||
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche']
|
||||
self.KEY_RIGHT_PRIMARY = \
|
||||
['d', 'Touche principale pour aller vers la droite']
|
||||
self.KEY_RIGHT_SECONDARY = \
|
||||
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
|
||||
self.KEY_ENTER = \
|
||||
['\n', 'Touche pour valider un menu']
|
||||
self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé']
|
||||
self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
|
||||
self.KEY_UP_SECONDARY = ['KEY_UP', 'Secondary key to move up']
|
||||
self.KEY_DOWN_PRIMARY = ['s', 'Main key to move down']
|
||||
self.KEY_DOWN_SECONDARY = ['KEY_DOWN', 'Secondary key to move down']
|
||||
self.KEY_LEFT_PRIMARY = ['q', 'Main key to move left']
|
||||
self.KEY_LEFT_SECONDARY = ['KEY_LEFT', 'Secondary key to move left']
|
||||
self.KEY_RIGHT_PRIMARY = ['d', 'Main key to move right']
|
||||
self.KEY_RIGHT_SECONDARY = ['KEY_RIGHT', 'Secondary key to move right']
|
||||
self.KEY_ENTER = ['\n', 'Key to validate a menu']
|
||||
self.KEY_INVENTORY = ['i', 'Key used to open the inventory']
|
||||
self.KEY_USE = ['u', 'Key used to use an item in the inventory']
|
||||
self.KEY_EQUIP = ['e', 'Key used to equip an item in the inventory']
|
||||
self.KEY_DROP = ['r', 'Key used to drop an item in the inventory']
|
||||
self.KEY_CHAT = ['t', 'Key used to talk to a friendly entity']
|
||||
self.KEY_WAIT = ['w', 'Key used to wait']
|
||||
self.KEY_LADDER = ['<', 'Key used to use ladders']
|
||||
self.KEY_LAUNCH = ['l', 'Key used to use a bow']
|
||||
self.KEY_DANCE = ['y', 'Key used to dance']
|
||||
self.TEXTURE_PACK = ['ascii', 'Texture pack']
|
||||
self.LOCALE = [locale.getlocale()[0][:2], 'Language']
|
||||
|
||||
def __getattribute__(self, item: str) -> Any:
|
||||
superattribute = super().__getattribute__(item)
|
||||
@ -47,32 +54,33 @@ class Settings:
|
||||
|
||||
def get_comment(self, item: str) -> str:
|
||||
"""
|
||||
Retrieve the comment of a setting.
|
||||
Retrieves the comment relative to a setting.
|
||||
"""
|
||||
if item in self.settings_keys:
|
||||
return object.__getattribute__(self, item)[1]
|
||||
return _(object.__getattribute__(self, item)[1])
|
||||
for key in self.settings_keys:
|
||||
if getattr(self, key) == item:
|
||||
return object.__getattribute__(self, key)[1]
|
||||
return _(object.__getattribute__(self, key)[1])
|
||||
|
||||
@property
|
||||
def settings_keys(self) -> Generator[str, Any, None]:
|
||||
"""
|
||||
Get the list of all parameters.
|
||||
Gets the list of all parameters.
|
||||
"""
|
||||
return (key for key in self.__dict__)
|
||||
|
||||
def loads_from_string(self, json_str: str) -> None:
|
||||
"""
|
||||
Dump settings
|
||||
Loads settings.
|
||||
"""
|
||||
d = json.loads(json_str)
|
||||
for key in d:
|
||||
setattr(self, key, d[key])
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, d[key])
|
||||
|
||||
def dumps_to_string(self) -> str:
|
||||
"""
|
||||
Dump settings
|
||||
Dumps settings.
|
||||
"""
|
||||
d = dict()
|
||||
for key in self.settings_keys:
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
from types import TracebackType
|
||||
|
||||
@ -5,7 +8,7 @@ from types import TracebackType
|
||||
class TermManager: # pragma: no cover
|
||||
"""
|
||||
The TermManager object initializes the terminal, returns a screen object and
|
||||
de-initializes the terminal after use
|
||||
de-initializes the terminal after use.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.screen = curses.initscr()
|
||||
@ -17,6 +20,8 @@ class TermManager: # pragma: no cover
|
||||
curses.cbreak()
|
||||
# make cursor invisible
|
||||
curses.curs_set(False)
|
||||
# Catch mouse events
|
||||
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
|
||||
# Enable colors
|
||||
curses.start_color()
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
@ -1,25 +1,32 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.entities.items import Bomb, Heart, Item
|
||||
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.interfaces import Entity, Map
|
||||
from squirrelbattle.resources import ResourceManager
|
||||
from ..entities.friendly import Chest, Trumpet
|
||||
from ..entities.items import BodySnatchPotion, Bomb, Explosion, Heart, Item
|
||||
from ..entities.monsters import GiantSeaEagle, Hedgehog, Rabbit, \
|
||||
TeddyBear, Tiger
|
||||
from ..entities.player import Player
|
||||
from ..interfaces import Entity, Map
|
||||
from ..resources import ResourceManager
|
||||
|
||||
|
||||
class TestEntities(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Load example map that can be used in tests.
|
||||
Loads example map that can be used in tests.
|
||||
"""
|
||||
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
|
||||
self.player = Player()
|
||||
self.player.constitution = 0
|
||||
self.map.add_entity(self.player)
|
||||
self.player.move(self.map.start_y, self.map.start_x)
|
||||
|
||||
def test_basic_entities(self) -> None:
|
||||
"""
|
||||
Test some random stuff with basic entities.
|
||||
Tests some random stuff with basic entities.
|
||||
"""
|
||||
entity = Entity()
|
||||
entity.move(42, 64)
|
||||
@ -34,43 +41,46 @@ class TestEntities(unittest.TestCase):
|
||||
|
||||
def test_fighting_entities(self) -> None:
|
||||
"""
|
||||
Test some random stuff with fighting entities.
|
||||
Tests some random stuff with fighting entities.
|
||||
"""
|
||||
entity = Beaver()
|
||||
entity = Tiger()
|
||||
self.map.add_entity(entity)
|
||||
self.assertEqual(entity.maxhealth, 20)
|
||||
self.assertEqual(entity.maxhealth, 30)
|
||||
self.assertEqual(entity.maxhealth, entity.health)
|
||||
self.assertEqual(entity.strength, 2)
|
||||
for _ in range(9):
|
||||
self.assertEqual(entity.strength, 5)
|
||||
for _ in range(5):
|
||||
self.assertEqual(entity.hit(entity),
|
||||
"beaver hits beaver. beaver takes 2 damage.")
|
||||
"Tiger hits tiger. Tiger takes 5 damage.")
|
||||
self.assertFalse(entity.dead)
|
||||
self.assertEqual(entity.hit(entity), "beaver hits beaver. "
|
||||
+ "beaver takes 2 damage. beaver dies.")
|
||||
self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
|
||||
+ "Tiger takes 5 damage. Tiger dies.")
|
||||
self.assertTrue(entity.dead)
|
||||
|
||||
entity = Rabbit()
|
||||
entity.health = 15
|
||||
entity.critical = 0
|
||||
self.map.add_entity(entity)
|
||||
entity.move(15, 44)
|
||||
# Move randomly
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
self.assertFalse(entity.y == 15 and entity.x == 44)
|
||||
|
||||
# Move to the player
|
||||
entity.move(3, 6)
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(entity.y == 2 and entity.x == 6)
|
||||
|
||||
# Rabbit should fight
|
||||
old_health = self.player.health
|
||||
self.map.tick()
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(entity.y == 2 and entity.x == 6)
|
||||
self.assertEqual(old_health - entity.strength, self.player.health)
|
||||
self.assertEqual(self.map.logs.messages[-1],
|
||||
f"{entity.name} hits {self.player.name}. \
|
||||
{self.player.name} takes {entity.strength} damage.")
|
||||
f"{entity.name.capitalize()} hits {self.player.name}. \
|
||||
{self.player.name.capitalize()} takes {entity.strength} damage.")
|
||||
|
||||
# Fight the rabbit
|
||||
self.player.critical = 0
|
||||
old_health = entity.health
|
||||
self.player.move_down()
|
||||
self.assertEqual(entity.health, old_health - self.player.strength)
|
||||
@ -85,29 +95,83 @@ class TestEntities(unittest.TestCase):
|
||||
self.assertTrue(entity.dead)
|
||||
self.assertGreaterEqual(self.player.current_xp, 3)
|
||||
|
||||
# Test that a chest is destroyed by a bomb
|
||||
bomb = Bomb()
|
||||
bomb.owner = self.player
|
||||
bomb.move(3, 6)
|
||||
self.map.add_entity(bomb)
|
||||
chest = Chest()
|
||||
chest.move(4, 6)
|
||||
self.map.add_entity(chest)
|
||||
bomb.exploding = True
|
||||
for _ in range(5):
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(chest.annihilated)
|
||||
|
||||
def test_familiar(self) -> None:
|
||||
fam = Trumpet()
|
||||
entity = Rabbit()
|
||||
self.map.add_entity(entity)
|
||||
self.map.add_entity(fam)
|
||||
self.player.move(1, 6)
|
||||
entity.move(2, 6)
|
||||
fam.move(2, 7)
|
||||
|
||||
# Test fighting
|
||||
entity.health = 2
|
||||
entity.paths = []
|
||||
entity.recalculate_paths()
|
||||
fam.target = entity
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(entity.dead)
|
||||
|
||||
# Test finding a new target
|
||||
entity2 = Rabbit()
|
||||
self.map.add_entity(entity2)
|
||||
entity2.move(2, 6)
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(fam.target == entity2)
|
||||
self.map.remove_entity(entity2)
|
||||
|
||||
# Test following the player and finding the player as target
|
||||
self.player.move(6, 5)
|
||||
fam.move(5, 5)
|
||||
fam.target = None
|
||||
self.player.move_down()
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(fam.target == self.player)
|
||||
self.assertEqual(fam.y, 6)
|
||||
self.assertEqual(fam.x, 5)
|
||||
|
||||
# Test random move
|
||||
fam.move(13, 20)
|
||||
fam.target = self.player
|
||||
self.map.tick(self.player)
|
||||
self.assertTrue(fam.x != 20 or fam.y != 13)
|
||||
|
||||
def test_items(self) -> None:
|
||||
"""
|
||||
Test some random stuff with items.
|
||||
Tests some random stuff with items.
|
||||
"""
|
||||
item = Item()
|
||||
self.map.add_entity(item)
|
||||
self.assertFalse(item.held)
|
||||
self.assertIsNone(item.held_by)
|
||||
item.hold(self.player)
|
||||
self.assertTrue(item.held)
|
||||
item.drop(2, 6)
|
||||
self.assertEqual(item.y, 2)
|
||||
self.assertEqual(item.held_by, self.player)
|
||||
item.drop()
|
||||
self.assertEqual(item.y, 1)
|
||||
self.assertEqual(item.x, 6)
|
||||
|
||||
# Pick up item
|
||||
self.player.move_down()
|
||||
self.assertTrue(item.held)
|
||||
self.player.move_left()
|
||||
self.player.move_right()
|
||||
self.assertEqual(item.held_by, self.player)
|
||||
self.assertIn(item, self.player.inventory)
|
||||
self.assertNotIn(item, self.map.entities)
|
||||
|
||||
def test_bombs(self) -> None:
|
||||
"""
|
||||
Test some random stuff with bombs.
|
||||
Tests some random stuff with bombs.
|
||||
"""
|
||||
item = Bomb()
|
||||
hedgehog = Hedgehog()
|
||||
@ -122,18 +186,37 @@ class TestEntities(unittest.TestCase):
|
||||
item.act(self.map)
|
||||
self.assertFalse(hedgehog.dead)
|
||||
self.assertFalse(teddy_bear.dead)
|
||||
item.drop(42, 42)
|
||||
self.player.move(42, 42)
|
||||
item.hold(self.player)
|
||||
item.use()
|
||||
self.assertEqual(item.y, 42)
|
||||
self.assertEqual(item.x, 42)
|
||||
item.act(self.map)
|
||||
# Wait for the explosion
|
||||
for _ignored in range(5):
|
||||
item.act(self.map)
|
||||
self.assertTrue(hedgehog.dead)
|
||||
self.assertTrue(teddy_bear.dead)
|
||||
bomb_state = item.save_state()
|
||||
self.assertEqual(bomb_state["damage"], item.damage)
|
||||
explosions = self.map.find_entities(Explosion)
|
||||
self.assertTrue(explosions)
|
||||
explosion = explosions[0]
|
||||
self.assertEqual(explosion.y, item.y)
|
||||
self.assertEqual(explosion.x, item.x)
|
||||
|
||||
# The player can't hold the explosion
|
||||
explosion.hold(self.player)
|
||||
self.assertNotIn(explosion, self.player.inventory)
|
||||
self.assertIsNone(explosion.held_by)
|
||||
|
||||
# The explosion disappears after two ticks
|
||||
explosion.act(self.map)
|
||||
explosion.act(self.map)
|
||||
self.assertNotIn(explosion, self.map.entities)
|
||||
|
||||
def test_hearts(self) -> None:
|
||||
"""
|
||||
Test some random stuff with hearts.
|
||||
Tests some random stuff with hearts.
|
||||
"""
|
||||
item = Heart()
|
||||
self.map.add_entity(item)
|
||||
@ -146,9 +229,27 @@ class TestEntities(unittest.TestCase):
|
||||
heart_state = item.save_state()
|
||||
self.assertEqual(heart_state["healing"], item.healing)
|
||||
|
||||
def test_body_snatch_potion(self) -> None:
|
||||
"""
|
||||
Tests some random stuff with body snatch potions.
|
||||
"""
|
||||
item = BodySnatchPotion()
|
||||
self.map.add_entity(item)
|
||||
item.hold(self.player)
|
||||
|
||||
tiger = Tiger(y=42, x=42)
|
||||
self.map.add_entity(tiger)
|
||||
|
||||
# The player becomes a tiger, and the tiger becomes a squirrel
|
||||
item.use()
|
||||
self.assertEqual(self.player.name, "tiger")
|
||||
self.assertEqual(tiger.name, "player")
|
||||
self.assertEqual(self.player.y, 42)
|
||||
self.assertEqual(self.player.x, 42)
|
||||
|
||||
def test_players(self) -> None:
|
||||
"""
|
||||
Test some random stuff with players.
|
||||
Tests some random stuff with players.
|
||||
"""
|
||||
player = Player()
|
||||
self.map.add_entity(player)
|
||||
@ -178,3 +279,26 @@ class TestEntities(unittest.TestCase):
|
||||
|
||||
player_state = player.save_state()
|
||||
self.assertEqual(player_state["current_xp"], 10)
|
||||
|
||||
player = Player()
|
||||
player.map = self.map
|
||||
player.add_xp(700)
|
||||
for _ in range(13):
|
||||
player.level_up()
|
||||
self.assertEqual(player.level, 12)
|
||||
self.assertEqual(player.critical, 5 + 95 // 30)
|
||||
self.assertEqual(player.charisma, 3)
|
||||
|
||||
def test_critical_hit(self) -> None:
|
||||
"""
|
||||
Ensure that critical hits are working.
|
||||
"""
|
||||
random.seed(2) # Next random.randint(1, 100) will output 8
|
||||
self.player.critical = 10
|
||||
sea_eagle = GiantSeaEagle()
|
||||
self.map.add_entity(sea_eagle)
|
||||
sea_eagle.move(2, 6)
|
||||
old_health = sea_eagle.health
|
||||
self.player.hit(sea_eagle)
|
||||
self.assertEqual(sea_eagle.health,
|
||||
old_health - self.player.strength * 4)
|
||||
|
@ -1,30 +1,60 @@
|
||||
import os
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import curses
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.bootstrap import Bootstrap
|
||||
from squirrelbattle.display.display import Display
|
||||
from squirrelbattle.display.display_manager import DisplayManager
|
||||
from squirrelbattle.entities.player import Player
|
||||
from squirrelbattle.game import Game, KeyValues, GameMode
|
||||
from squirrelbattle.menus import MainMenuValues
|
||||
from squirrelbattle.settings import Settings
|
||||
from ..bootstrap import Bootstrap
|
||||
from ..display.display import Display
|
||||
from ..display.display_manager import DisplayManager
|
||||
from ..entities.friendly import Chest, Merchant, Sunflower
|
||||
from ..entities.items import Bomb, Bow, Chestplate, Explosion, FireBallStaff, \
|
||||
Heart, Helmet, Monocle, RingCritical, ScrollofDamage, ScrollofWeakening, \
|
||||
Shield, Sword
|
||||
from ..entities.monsters import GiantSeaEagle, Rabbit
|
||||
from ..entities.player import Player
|
||||
from ..enums import DisplayActions, GameMode, KeyValues
|
||||
from ..game import Game
|
||||
from ..interfaces import Map, Tile
|
||||
from ..menus import MainMenuValues
|
||||
from ..resources import ResourceManager
|
||||
from ..settings import Settings
|
||||
from ..translations import gettext as _, Translator
|
||||
|
||||
|
||||
class TestGame(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Setup game.
|
||||
Sets the game up.
|
||||
"""
|
||||
self.game = Game()
|
||||
self.game.new_game()
|
||||
self.game.map = Map.load(
|
||||
ResourceManager.get_asset_path("example_map.txt"))
|
||||
self.game.map.add_entity(self.game.player)
|
||||
self.game.player.move(self.game.map.start_y, self.game.map.start_x)
|
||||
self.game.logs.add_message("Hello World !")
|
||||
display = DisplayManager(None, self.game)
|
||||
self.game.display_actions = display.handle_display_action
|
||||
|
||||
def test_load_game(self) -> None:
|
||||
"""
|
||||
Save a game and reload it.
|
||||
Saves a game and reloads it.
|
||||
"""
|
||||
bomb = Bomb()
|
||||
self.game.map.add_entity(bomb)
|
||||
sword = Sword()
|
||||
self.game.map.add_entity(sword)
|
||||
# Add items in the inventory to check that it is well loaded
|
||||
bomb.hold(self.game.player)
|
||||
sword.hold(self.game.player)
|
||||
sword.equip()
|
||||
|
||||
# Ensure that merchants can be saved
|
||||
merchant = Merchant()
|
||||
merchant.move(3, 6)
|
||||
self.game.map.add_entity(merchant)
|
||||
|
||||
old_state = self.game.save_state()
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
@ -37,19 +67,43 @@ class TestGame(unittest.TestCase):
|
||||
|
||||
new_state = self.game.save_state()
|
||||
self.assertEqual(old_state, new_state)
|
||||
self.assertIsNone(self.game.message)
|
||||
|
||||
# Ensure that the bomb is loaded
|
||||
self.assertTrue(self.game.player.inventory)
|
||||
|
||||
# Error on loading save
|
||||
with open(ResourceManager.get_config_path("save.json"), "w") as f:
|
||||
f.write("I am not a JSON file")
|
||||
self.assertIsNone(self.game.message)
|
||||
self.game.load_game()
|
||||
self.assertIsNotNone(self.game.message)
|
||||
self.game.message = None
|
||||
|
||||
with open(ResourceManager.get_config_path("save.json"), "w") as f:
|
||||
f.write("{}")
|
||||
self.assertIsNone(self.game.message)
|
||||
self.game.load_game()
|
||||
self.assertIsNotNone(self.game.message)
|
||||
self.game.message = None
|
||||
|
||||
# Load game with a dead player
|
||||
self.game.map.remove_entity(self.game.player)
|
||||
self.game.save_game()
|
||||
self.game.load_game()
|
||||
self.assertIsNotNone(self.game.message)
|
||||
|
||||
def test_bootstrap_fail(self) -> None:
|
||||
"""
|
||||
Ensure that the test can't play the game,
|
||||
Ensures that the test can't play the game,
|
||||
because there is no associated shell.
|
||||
Yeah, that's only for coverage.
|
||||
"""
|
||||
self.assertRaises(Exception, Bootstrap.run_game)
|
||||
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
|
||||
|
||||
def test_key_translation(self) -> None:
|
||||
"""
|
||||
Test key bindings.
|
||||
Tests key bindings.
|
||||
"""
|
||||
self.game.settings = Settings()
|
||||
|
||||
@ -80,14 +134,38 @@ class TestGame(unittest.TestCase):
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_ENTER, self.game.settings),
|
||||
KeyValues.ENTER)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_INVENTORY, self.game.settings),
|
||||
KeyValues.INVENTORY)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_CHAT, self.game.settings),
|
||||
KeyValues.CHAT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_USE, self.game.settings),
|
||||
KeyValues.USE)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_EQUIP, self.game.settings),
|
||||
KeyValues.EQUIP)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_DROP, self.game.settings),
|
||||
KeyValues.DROP)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_WAIT, self.game.settings),
|
||||
KeyValues.WAIT)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_LADDER, self.game.settings),
|
||||
KeyValues.LADDER)
|
||||
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
|
||||
KeyValues.SPACE)
|
||||
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
|
||||
None)
|
||||
self.assertEqual(KeyValues.translate_key(
|
||||
self.game.settings.KEY_DANCE, self.game.settings),
|
||||
KeyValues.DANCE)
|
||||
|
||||
def test_key_press(self) -> None:
|
||||
"""
|
||||
Press a key and see what is done.
|
||||
Presses a key and asserts what is done is correct.
|
||||
"""
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
self.assertEqual(self.game.main_menu.validate(),
|
||||
@ -144,6 +222,12 @@ class TestGame(unittest.TestCase):
|
||||
self.game.map.remove_entity(entity)
|
||||
|
||||
y, x = self.game.player.y, self.game.player.x
|
||||
|
||||
# Ensure that the neighborhood is walkable
|
||||
for dx in [-1, 0, 1]:
|
||||
for dy in [-1, 0, 1]:
|
||||
self.game.map.tiles[y + dy][x + dx] = Tile.FLOOR
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
new_y, new_x = self.game.player.y, self.game.player.x
|
||||
self.assertEqual(new_y, y + 1)
|
||||
@ -167,12 +251,75 @@ class TestGame(unittest.TestCase):
|
||||
self.assertEqual(new_y, y)
|
||||
self.assertEqual(new_x, x - 1)
|
||||
|
||||
explosion = Explosion()
|
||||
self.game.map.add_entity(explosion)
|
||||
self.assertIn(explosion, self.game.map.entities)
|
||||
self.game.handle_key_pressed(KeyValues.WAIT)
|
||||
self.game.handle_key_pressed(KeyValues.WAIT)
|
||||
self.assertNotIn(explosion, self.game.map.entities)
|
||||
|
||||
rabbit = Rabbit()
|
||||
self.game.map.add_entity(rabbit)
|
||||
self.game.player.move(1, 6)
|
||||
rabbit.move(3, 6)
|
||||
self.game.player.charisma = 11
|
||||
self.game.handle_key_pressed(KeyValues.DANCE)
|
||||
self.assertEqual(rabbit.confused, 1)
|
||||
string = rabbit.hit(self.game.player)
|
||||
self.assertEqual(
|
||||
string, _("{name} is confused, it can not hit {opponent}.")
|
||||
.format(name=rabbit.translated_name.capitalize(),
|
||||
opponent=self.game.player.translated_name))
|
||||
rabbit.confused = 0
|
||||
self.game.player.charisma = 0
|
||||
self.game.handle_key_pressed(KeyValues.DANCE)
|
||||
self.assertEqual(rabbit.confused, 0)
|
||||
rabbit.die()
|
||||
|
||||
self.game.player.charisma = 11
|
||||
self.game.handle_key_pressed(KeyValues.DANCE)
|
||||
self.game.player.charisma = 1
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
|
||||
def test_mouse_click(self) -> None:
|
||||
"""
|
||||
Simulates mouse clicks.
|
||||
"""
|
||||
self.game.state = GameMode.MAINMENU
|
||||
|
||||
# Change the color of the artwork
|
||||
self.game.display_actions(DisplayActions.MOUSE, 0, 10,
|
||||
curses.BUTTON1_CLICKED)
|
||||
|
||||
# Settings menu
|
||||
self.game.display_actions(DisplayActions.MOUSE, 25, 21,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertEqual(self.game.main_menu.position, 4)
|
||||
self.assertEqual(self.game.state, GameMode.SETTINGS)
|
||||
|
||||
bomb = Bomb()
|
||||
bomb.hold(self.game.player)
|
||||
bomb2 = Bomb()
|
||||
bomb2.hold(self.game.player)
|
||||
|
||||
self.game.state = GameMode.INVENTORY
|
||||
|
||||
# Click nowhere
|
||||
self.game.display_actions(DisplayActions.MOUSE, 0, 0,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertEqual(self.game.state, GameMode.INVENTORY)
|
||||
|
||||
# Click on the second item
|
||||
self.game.display_actions(DisplayActions.MOUSE, 8, 25,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertEqual(self.game.state, GameMode.INVENTORY)
|
||||
self.assertEqual(self.game.inventory_menu.position, 1)
|
||||
|
||||
def test_new_game(self) -> None:
|
||||
"""
|
||||
Ensure that the start button starts a new game.
|
||||
Ensures that the start button starts a new game.
|
||||
"""
|
||||
old_map = self.game.map
|
||||
old_player = self.game.player
|
||||
@ -195,7 +342,7 @@ class TestGame(unittest.TestCase):
|
||||
|
||||
def test_settings_menu(self) -> None:
|
||||
"""
|
||||
Ensure that the settings menu is working properly.
|
||||
Ensures that the settings menu is working properly.
|
||||
"""
|
||||
self.game.settings = Settings()
|
||||
|
||||
@ -207,13 +354,13 @@ class TestGame(unittest.TestCase):
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.SETTINGS)
|
||||
|
||||
# Define the "move up" key to 'w'
|
||||
# Define the "move up" key to 'h'
|
||||
self.assertFalse(self.game.settings_menu.waiting_for_key)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertTrue(self.game.settings_menu.waiting_for_key)
|
||||
self.game.handle_key_pressed(None, 'w')
|
||||
self.game.handle_key_pressed(None, 'h')
|
||||
self.assertFalse(self.game.settings_menu.waiting_for_key)
|
||||
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
|
||||
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'h')
|
||||
|
||||
# Navigate to "move left"
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
@ -234,11 +381,8 @@ class TestGame(unittest.TestCase):
|
||||
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
|
||||
|
||||
# Navigate to "texture pack"
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
for ignored in range(14):
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
# Change texture pack
|
||||
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
|
||||
@ -247,12 +391,26 @@ class TestGame(unittest.TestCase):
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
|
||||
|
||||
# Change language
|
||||
Translator.compilemessages()
|
||||
Translator.refresh_translations()
|
||||
self.game.settings.LOCALE = "en"
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.LOCALE, "fr")
|
||||
self.assertEqual(_("New game"), "Nouvelle partie")
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.LOCALE, "de")
|
||||
self.assertEqual(_("New game"), "Neu Spiel")
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.LOCALE, "es")
|
||||
self.assertEqual(_("New game"), "Nuevo partido")
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.settings.LOCALE, "en")
|
||||
self.assertEqual(_("New game"), "New game")
|
||||
|
||||
# Navigate to "back" button
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
@ -270,7 +428,7 @@ class TestGame(unittest.TestCase):
|
||||
|
||||
def test_dead_screen(self) -> None:
|
||||
"""
|
||||
Kill player and render dead screen.
|
||||
Kills the player and renders the dead message on the fake screen.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
# Kill player
|
||||
@ -286,6 +444,518 @@ class TestGame(unittest.TestCase):
|
||||
|
||||
def test_not_implemented(self) -> None:
|
||||
"""
|
||||
Check that some functions are not implemented, only for coverage.
|
||||
Checks that some functions are not implemented, only for coverage.
|
||||
"""
|
||||
self.assertRaises(NotImplementedError, Display.display, None)
|
||||
self.assertRaises(NotImplementedError, Display.update, None, self.game)
|
||||
|
||||
def test_messages(self) -> None:
|
||||
"""
|
||||
Displays error messages.
|
||||
"""
|
||||
self.game.message = "I am an error"
|
||||
self.game.display_actions(DisplayActions.UPDATE)
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
self.game.handle_key_pressed(None, "random key")
|
||||
self.assertIsNone(self.game.message)
|
||||
|
||||
def test_inventory_menu(self) -> None:
|
||||
"""
|
||||
Opens the inventory menu and interacts with items.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
# Open and close the inventory
|
||||
self.game.handle_key_pressed(KeyValues.INVENTORY)
|
||||
self.assertEqual(self.game.state, GameMode.INVENTORY)
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
|
||||
# Add five bombs in the inventory
|
||||
for ignored in range(5):
|
||||
bomb = Bomb()
|
||||
bomb.map = self.game.map
|
||||
bomb.map.add_entity(bomb)
|
||||
bomb.hold(self.game.player)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.INVENTORY)
|
||||
self.assertEqual(self.game.state, GameMode.INVENTORY)
|
||||
|
||||
# Navigate in the menu
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.inventory_menu.position, 3)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.inventory_menu.position, 4)
|
||||
|
||||
# Equip key does nothing
|
||||
self.game.handle_key_pressed(KeyValues.EQUIP)
|
||||
|
||||
# Drop an item
|
||||
bomb = self.game.player.inventory[-1]
|
||||
self.assertEqual(self.game.inventory_menu.validate(), bomb)
|
||||
self.assertEqual(bomb.held_by, self.game.player)
|
||||
self.game.handle_key_pressed(KeyValues.DROP)
|
||||
self.assertIsNone(bomb.held_by)
|
||||
self.assertIsNone(bomb.owner)
|
||||
self.assertFalse(bomb.exploding)
|
||||
self.assertEqual(bomb.y, self.game.player.y)
|
||||
self.assertEqual(bomb.x, self.game.player.x)
|
||||
|
||||
# Use the bomb
|
||||
bomb = self.game.player.inventory[-1]
|
||||
self.assertEqual(self.game.inventory_menu.validate(), bomb)
|
||||
self.assertEqual(bomb.held_by, self.game.player)
|
||||
self.game.handle_key_pressed(KeyValues.USE)
|
||||
self.assertIsNone(bomb.held_by)
|
||||
self.assertEqual(bomb.owner, self.game.player)
|
||||
self.assertTrue(bomb.exploding)
|
||||
self.assertEqual(bomb.y, self.game.player.y)
|
||||
self.assertEqual(bomb.x, self.game.player.x)
|
||||
|
||||
def test_talk_to_sunflowers(self) -> None:
|
||||
"""
|
||||
Interacts with sunflowers.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
sunflower = Sunflower()
|
||||
sunflower.move(self.game.player.y + 1, self.game.player.x)
|
||||
self.game.map.add_entity(sunflower)
|
||||
|
||||
# Does nothing
|
||||
self.assertIsNone(self.game.handle_friendly_entity_chat(KeyValues.UP))
|
||||
|
||||
# Talk to sunflower... or not
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.assertTrue(self.game.waiting_for_friendly_key)
|
||||
# Wrong key
|
||||
self.game.handle_key_pressed(KeyValues.EQUIP)
|
||||
self.assertFalse(self.game.waiting_for_friendly_key)
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.assertTrue(self.game.waiting_for_friendly_key)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertFalse(self.game.waiting_for_friendly_key)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
self.assertFalse(len(self.game.logs.messages) > 1)
|
||||
|
||||
# Talk to sunflower
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.assertTrue(self.game.waiting_for_friendly_key)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertFalse(self.game.waiting_for_friendly_key)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
self.assertTrue(self.game.logs.messages)
|
||||
# Ensure that the message is a good message
|
||||
self.assertTrue(any(self.game.logs.messages[1].endswith(msg)
|
||||
for msg in Sunflower().dialogue_option))
|
||||
|
||||
# Test all directions to detect the friendly entity
|
||||
self.game.player.move(sunflower.y + 1, sunflower.x)
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(len(self.game.logs.messages), 3)
|
||||
self.game.player.move(sunflower.y, sunflower.x + 1)
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
self.assertEqual(len(self.game.logs.messages), 4)
|
||||
self.game.player.move(sunflower.y, sunflower.x - 1)
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.game.handle_key_pressed(KeyValues.RIGHT)
|
||||
self.assertEqual(len(self.game.logs.messages), 5)
|
||||
|
||||
def test_talk_to_merchant(self) -> None:
|
||||
"""
|
||||
Interacts with merchants.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
merchant = Merchant()
|
||||
merchant.move(self.game.player.y + 1, self.game.player.x)
|
||||
self.game.map.add_entity(merchant)
|
||||
|
||||
# Does nothing
|
||||
self.assertIsNone(self.game.handle_friendly_entity_chat(KeyValues.UP))
|
||||
|
||||
# Talk to merchant
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.assertTrue(self.game.waiting_for_friendly_key)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertFalse(self.game.waiting_for_friendly_key)
|
||||
self.assertEqual(self.game.state, GameMode.STORE)
|
||||
self.assertTrue(self.game.logs.messages)
|
||||
|
||||
# Navigate in the menu
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
self.assertFalse(self.game.is_in_store_menu)
|
||||
self.game.handle_key_pressed(KeyValues.RIGHT)
|
||||
self.assertTrue(self.game.is_in_store_menu)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.store_menu.position, 1)
|
||||
|
||||
self.game.player.hazel = 0x7ffff42ff
|
||||
|
||||
# The second item is not a heart
|
||||
merchant.inventory[1] = sword = Sword()
|
||||
# Buy the second item by clicking on it
|
||||
item = self.game.store_menu.validate()
|
||||
self.assertIn(item, merchant.inventory)
|
||||
self.game.display_actions(DisplayActions.MOUSE, 7, 25,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertIn(item, self.game.player.inventory)
|
||||
self.assertNotIn(item, merchant.inventory)
|
||||
|
||||
# Buy a heart
|
||||
merchant.inventory[1] = Heart()
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
item = self.game.store_menu.validate()
|
||||
self.assertIn(item, merchant.inventory)
|
||||
self.assertEqual(item, merchant.inventory[1])
|
||||
self.game.player.health = self.game.player.maxhealth - 1 - item.healing
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertNotIn(item, self.game.player.inventory)
|
||||
self.assertNotIn(item, merchant.inventory)
|
||||
self.assertEqual(self.game.player.health,
|
||||
self.game.player.maxhealth - 1)
|
||||
|
||||
# We don't have enough of money
|
||||
self.game.player.hazel = 0
|
||||
item = self.game.store_menu.validate()
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertNotIn(item, self.game.player.inventory)
|
||||
self.assertIn(item, merchant.inventory)
|
||||
self.assertEqual(self.game.message,
|
||||
_("The buyer does not have enough money"))
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
# Sell an item
|
||||
self.game.inventory_menu.position = len(self.game.player.inventory) - 1
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
self.assertFalse(self.game.is_in_store_menu)
|
||||
self.assertIn(sword, self.game.player.inventory)
|
||||
self.assertEqual(self.game.inventory_menu.validate(), sword)
|
||||
old_player_money, old_merchant_money = self.game.player.hazel,\
|
||||
merchant.hazel
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertNotIn(sword, self.game.player.inventory)
|
||||
self.assertIn(sword, merchant.inventory)
|
||||
self.assertEqual(self.game.player.hazel, old_player_money + sword.price)
|
||||
self.assertEqual(merchant.hazel, old_merchant_money - sword.price)
|
||||
|
||||
# Exit the menu
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
|
||||
def test_equipment(self) -> None:
|
||||
"""
|
||||
Ensure that equipment is working.
|
||||
"""
|
||||
self.game.state = GameMode.INVENTORY
|
||||
|
||||
# sword goes into the main equipment slot
|
||||
sword = Sword()
|
||||
sword.hold(self.game.player)
|
||||
self.game.handle_key_pressed(KeyValues.EQUIP)
|
||||
self.assertEqual(self.game.player.equipped_main, sword)
|
||||
|
||||
# shield goes into the secondary equipment slot
|
||||
shield = Shield()
|
||||
shield.hold(self.game.player)
|
||||
shield.equip()
|
||||
self.assertEqual(self.game.player.equipped_secondary, shield)
|
||||
|
||||
# helmet goes into the helmet slot
|
||||
helmet = Helmet()
|
||||
helmet.hold(self.game.player)
|
||||
helmet.equip()
|
||||
self.assertEqual(self.game.player.equipped_helmet, helmet)
|
||||
|
||||
# helmet goes into the armor slot
|
||||
chestplate = Chestplate()
|
||||
chestplate.hold(self.game.player)
|
||||
chestplate.equip()
|
||||
self.assertEqual(self.game.player.equipped_armor, chestplate)
|
||||
|
||||
# Use bomb
|
||||
bomb = Bomb()
|
||||
bomb.hold(self.game.player)
|
||||
bomb.equip()
|
||||
self.assertEqual(self.game.player.equipped_secondary, bomb)
|
||||
self.assertFalse(shield.equipped)
|
||||
self.game.state = GameMode.PLAY
|
||||
self.game.handle_key_pressed(KeyValues.USE)
|
||||
self.assertIsNone(self.game.player.equipped_secondary)
|
||||
self.game.state = GameMode.INVENTORY
|
||||
shield.equip()
|
||||
self.assertEqual(self.game.player.equipped_secondary, shield)
|
||||
|
||||
# Reequip, which is useless but covers code
|
||||
sword.equip()
|
||||
shield.equip()
|
||||
helmet.equip()
|
||||
chestplate.equip()
|
||||
self.game.save_state()
|
||||
|
||||
# Unequip all
|
||||
sword.unequip()
|
||||
shield.unequip()
|
||||
helmet.unequip()
|
||||
chestplate.unequip()
|
||||
self.assertIsNone(self.game.player.equipped_main)
|
||||
self.assertIsNone(self.game.player.equipped_secondary)
|
||||
self.assertIsNone(self.game.player.equipped_helmet)
|
||||
self.assertIsNone(self.game.player.equipped_armor)
|
||||
self.assertIn(sword, self.game.player.inventory)
|
||||
self.assertIn(shield, self.game.player.inventory)
|
||||
self.assertIn(helmet, self.game.player.inventory)
|
||||
self.assertIn(chestplate, self.game.player.inventory)
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
|
||||
# Test rings
|
||||
self.game.player.inventory.clear()
|
||||
ring = RingCritical()
|
||||
ring.hold(self.game.player)
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
old_critical = self.game.player.critical
|
||||
self.game.handle_key_pressed(KeyValues.EQUIP)
|
||||
self.assertEqual(self.game.player.critical,
|
||||
old_critical + ring.critical)
|
||||
self.game.save_state()
|
||||
ring.unequip()
|
||||
|
||||
def test_monocle(self) -> None:
|
||||
"""
|
||||
The player is wearing a monocle, then the stats are displayed.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
monocle = Monocle()
|
||||
monocle.hold(self.game.player)
|
||||
monocle.equip()
|
||||
|
||||
sea_eagle = GiantSeaEagle()
|
||||
self.game.map.add_entity(sea_eagle)
|
||||
sea_eagle.move(2, 6)
|
||||
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
|
||||
def test_ladders(self) -> None:
|
||||
"""
|
||||
Ensure that the player can climb on ladders.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
self.assertEqual(self.game.player.map.floor, 0)
|
||||
self.game.handle_key_pressed(KeyValues.LADDER)
|
||||
self.assertEqual(self.game.player.map.floor, 0)
|
||||
|
||||
# Move nowhere
|
||||
self.game.player.move(10, 10)
|
||||
self.game.handle_key_pressed(KeyValues.LADDER)
|
||||
self.assertEqual(self.game.player.map.floor, 0)
|
||||
|
||||
# Move down
|
||||
self.game.player.move(3, 40) # Move on a ladder
|
||||
self.game.handle_key_pressed(KeyValues.LADDER)
|
||||
self.assertEqual(self.game.map_index, 1)
|
||||
self.assertEqual(self.game.player.map.floor, 1)
|
||||
self.game.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
# Move up
|
||||
self.game.handle_key_pressed(KeyValues.LADDER)
|
||||
self.assertEqual(self.game.player.map.floor, 0)
|
||||
self.assertEqual(self.game.player.y, 3)
|
||||
self.assertEqual(self.game.player.x, 40)
|
||||
self.game.display_actions(DisplayActions.UPDATE)
|
||||
|
||||
def test_credits(self) -> None:
|
||||
"""
|
||||
Load credits menu.
|
||||
"""
|
||||
self.game.state = GameMode.MAINMENU
|
||||
|
||||
self.game.display_actions(DisplayActions.MOUSE, 41, 41,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertEqual(self.game.state, GameMode.CREDITS)
|
||||
self.game.display_actions(DisplayActions.MOUSE, 21, 21,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
|
||||
self.game.state = GameMode.CREDITS
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
|
||||
self.assertEqual(self.game.state, GameMode.MAINMENU)
|
||||
|
||||
def test_launch(self) -> None:
|
||||
"""
|
||||
Use the long range weapons to kill some entities.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
self.game.player.move(2, 6)
|
||||
|
||||
b = Bow()
|
||||
b.held_by = self.game.player
|
||||
self.game.player.equipped_main = b
|
||||
self.assertTrue(self.game.player.equipped_main)
|
||||
|
||||
entity = Rabbit()
|
||||
entity.health = 1
|
||||
self.game.map.add_entity(entity)
|
||||
entity.move(3, 6)
|
||||
|
||||
self.game.handle_launch(KeyValues.UP)
|
||||
|
||||
self.game.waiting_for_launch_key = True
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
|
||||
entity = Rabbit()
|
||||
entity.health = 1
|
||||
self.game.map.add_entity(entity)
|
||||
entity.move(2, 8)
|
||||
self.game.waiting_for_launch_key = True
|
||||
self.game.handle_key_pressed(KeyValues.RIGHT)
|
||||
|
||||
entity = Rabbit()
|
||||
entity.health = 1
|
||||
self.game.map.add_entity(entity)
|
||||
entity.move(2, 5)
|
||||
self.game.waiting_for_launch_key = True
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
|
||||
key = "l"
|
||||
KeyValues.translate_key(key, self.game.settings)
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.LAUNCH)
|
||||
self.assertTrue(self.game.waiting_for_launch_key)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
|
||||
self.assertTrue(entity.dead)
|
||||
|
||||
entity2 = Rabbit()
|
||||
entity2.health = 1
|
||||
self.game.map.add_entity(entity2)
|
||||
entity2.move(1, 6)
|
||||
|
||||
b = FireBallStaff()
|
||||
self.game.player.inventory.append(b)
|
||||
b.held_by = self.game.player
|
||||
b.equip()
|
||||
|
||||
self.game.handle_key_pressed(KeyValues.LAUNCH)
|
||||
self.assertTrue(self.game.waiting_for_launch_key)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
|
||||
self.assertTrue(entity2.dead)
|
||||
|
||||
def test_scrolls(self) -> None:
|
||||
"""
|
||||
Use the scrolls.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
self.game.player.move(2, 6)
|
||||
|
||||
entity = Rabbit()
|
||||
self.game.map.add_entity(entity)
|
||||
entity.move(3, 6)
|
||||
|
||||
entity2 = GiantSeaEagle()
|
||||
self.game.map.add_entity(entity2)
|
||||
entity2.move(3, 8)
|
||||
|
||||
scroll1 = ScrollofDamage()
|
||||
scroll2 = ScrollofWeakening()
|
||||
self.game.player.inventory.append(scroll1)
|
||||
self.game.player.inventory.append(scroll2)
|
||||
scroll1.held_by = self.game.player
|
||||
scroll2.held_by = self.game.player
|
||||
|
||||
scroll1.use()
|
||||
self.assertTrue(entity.health != entity.maxhealth)
|
||||
self.assertTrue(entity2.health != entity2.maxhealth)
|
||||
|
||||
scroll2.use()
|
||||
self.assertEqual(entity.strength, 0)
|
||||
self.assertEqual(entity2.strength, 999)
|
||||
|
||||
self.game.map.tick(self.game.player)
|
||||
self.game.map.tick(self.game.player)
|
||||
self.game.map.tick(self.game.player)
|
||||
|
||||
self.assertEqual(entity2.effects, [])
|
||||
|
||||
def test_chests(self) -> None:
|
||||
"""
|
||||
Interacts with chests.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
chest = Chest()
|
||||
chest.move(2, 6)
|
||||
self.game.map.add_entity(chest)
|
||||
chest.inventory.append(FireBallStaff())
|
||||
|
||||
# Talk to merchant
|
||||
self.game.handle_key_pressed(KeyValues.CHAT)
|
||||
self.assertTrue(self.game.waiting_for_friendly_key)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertFalse(self.game.waiting_for_friendly_key)
|
||||
self.assertEqual(self.game.state, GameMode.CHEST)
|
||||
self.assertTrue(self.game.logs.messages)
|
||||
|
||||
# Navigate in the menu
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
self.assertFalse(self.game.is_in_chest_menu)
|
||||
self.game.handle_key_pressed(KeyValues.RIGHT)
|
||||
self.assertTrue(self.game.is_in_chest_menu)
|
||||
self.game.handle_key_pressed(KeyValues.UP)
|
||||
self.assertEqual(self.game.chest_menu.position, 1)
|
||||
|
||||
# The second item is not a heart
|
||||
chest.inventory[1] = sword = Sword()
|
||||
# Take the second item
|
||||
item = self.game.chest_menu.validate()
|
||||
self.assertIn(item, chest.inventory)
|
||||
self.game.display_actions(DisplayActions.MOUSE, 7, 25,
|
||||
curses.BUTTON1_CLICKED)
|
||||
self.assertIn(item, self.game.player.inventory)
|
||||
self.assertNotIn(item, chest.inventory)
|
||||
|
||||
# Give an item back
|
||||
self.game.inventory_menu.position = len(self.game.player.inventory) - 1
|
||||
self.game.handle_key_pressed(KeyValues.LEFT)
|
||||
self.assertFalse(self.game.is_in_chest_menu)
|
||||
self.assertIn(sword, self.game.player.inventory)
|
||||
self.assertEqual(self.game.inventory_menu.validate(), sword)
|
||||
self.game.handle_key_pressed(KeyValues.ENTER)
|
||||
self.assertNotIn(sword, self.game.player.inventory)
|
||||
self.assertIn(sword, chest.inventory)
|
||||
|
||||
# Test immortality
|
||||
self.game.player.hit(chest)
|
||||
self.assertTrue(not chest.dead)
|
||||
|
||||
# Exit the menu
|
||||
self.game.handle_key_pressed(KeyValues.SPACE)
|
||||
self.assertEqual(self.game.state, GameMode.PLAY)
|
||||
|
||||
def test_doors(self) -> None:
|
||||
"""
|
||||
Check that the user can open doors.
|
||||
"""
|
||||
self.game.state = GameMode.PLAY
|
||||
|
||||
self.game.player.move(9, 8)
|
||||
self.assertEqual(self.game.map.tiles[10][8], Tile.DOOR)
|
||||
# Open door
|
||||
self.game.handle_key_pressed(KeyValues.DOWN)
|
||||
self.assertEqual(self.game.map.tiles[10][8], Tile.FLOOR)
|
||||
self.assertEqual(self.game.player.y, 10)
|
||||
self.assertEqual(self.game.player.x, 8)
|
||||
self.game.display_actions(DisplayActions.REFRESH)
|
||||
|
@ -1,14 +1,17 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.display.texturepack import TexturePack
|
||||
from squirrelbattle.interfaces import Map, Tile
|
||||
from squirrelbattle.resources import ResourceManager
|
||||
from ..display.texturepack import TexturePack
|
||||
from ..interfaces import Map, Slope, Tile
|
||||
from ..resources import ResourceManager
|
||||
|
||||
|
||||
class TestInterfaces(unittest.TestCase):
|
||||
def test_map(self) -> None:
|
||||
"""
|
||||
Create a map and check that it is well parsed.
|
||||
Creates a map and checks that it is well parsed.
|
||||
"""
|
||||
m = Map.load_from_string("0 0\n.#\n#.\n")
|
||||
self.assertEqual(m.width, 2)
|
||||
@ -17,7 +20,7 @@ class TestInterfaces(unittest.TestCase):
|
||||
|
||||
def test_load_map(self) -> None:
|
||||
"""
|
||||
Try to load a map from a file.
|
||||
Tries to load a map from a file.
|
||||
"""
|
||||
m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
|
||||
self.assertEqual(m.width, 52)
|
||||
@ -25,7 +28,7 @@ class TestInterfaces(unittest.TestCase):
|
||||
|
||||
def test_tiles(self) -> None:
|
||||
"""
|
||||
Test some things about tiles.
|
||||
Tests some things about tiles.
|
||||
"""
|
||||
self.assertFalse(Tile.FLOOR.is_wall())
|
||||
self.assertTrue(Tile.WALL.is_wall())
|
||||
@ -34,3 +37,21 @@ class TestInterfaces(unittest.TestCase):
|
||||
self.assertFalse(Tile.WALL.can_walk())
|
||||
self.assertFalse(Tile.EMPTY.can_walk())
|
||||
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')
|
||||
|
||||
def test_slope(self) -> None:
|
||||
"""
|
||||
Test good behaviour of slopes (basically vectors, compared according to
|
||||
the determinant)
|
||||
"""
|
||||
a = Slope(1, 1)
|
||||
b = Slope(0, 1)
|
||||
self.assertTrue(b < a)
|
||||
self.assertTrue(b <= a)
|
||||
self.assertTrue(a <= a)
|
||||
self.assertTrue(a == a)
|
||||
self.assertTrue(a > b)
|
||||
self.assertTrue(a >= b)
|
||||
|
||||
# def test_visibility(self) -> None:
|
||||
# m = Map.load(ResourceManager.get_asset_path("example_map_3.txt"))
|
||||
# m.compute_visibility(1, 1, 50)
|
||||
|
56
squirrelbattle/tests/mapgeneration_test.py
Normal file
56
squirrelbattle/tests/mapgeneration_test.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import randint
|
||||
from typing import List
|
||||
import unittest
|
||||
|
||||
from ..display.texturepack import TexturePack
|
||||
from ..interfaces import Map, Tile
|
||||
from ..mapgeneration import broguelike
|
||||
|
||||
|
||||
class TestBroguelike(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.generator = broguelike.Generator()
|
||||
self.stom = lambda x: Map.load_from_string("0 0\n" + x)
|
||||
self.mtos = lambda x: x.draw_string(TexturePack.ASCII_PACK)
|
||||
|
||||
def test_dist(self) -> None:
|
||||
m = self.stom(".. ..\n ... ")
|
||||
distance = broguelike.dist(m.tiles, 0, 0, 0, 4)
|
||||
self.assertEqual(distance, 6)
|
||||
m = self.stom(". .")
|
||||
distance = broguelike.dist(m.tiles, 0, 0, 0, 2)
|
||||
self.assertEqual(distance, -1)
|
||||
|
||||
def is_connex(self, grid: List[List[Tile]]) -> bool:
|
||||
h, w = len(grid), len(grid[0])
|
||||
y, x = -1, -1
|
||||
while not grid[y][x].can_walk():
|
||||
y, x = randint(0, h - 1), randint(0, w - 1)
|
||||
queue = Map.neighbourhood(grid, y, x)
|
||||
while queue:
|
||||
y, x = queue.pop()
|
||||
if grid[y][x].can_walk() or grid[y][x] == Tile.DOOR:
|
||||
grid[y][x] = Tile.WALL
|
||||
queue += Map.neighbourhood(grid, y, x)
|
||||
return not any([t.can_walk() or t == Tile.DOOR
|
||||
for row in grid for t in row])
|
||||
|
||||
def test_build_doors(self) -> None:
|
||||
m = self.stom(". .\n. .\n. .\n")
|
||||
self.assertFalse(self.generator.build_door(m.tiles, 1, 1, 0, 1, 2))
|
||||
|
||||
def test_connexity(self) -> None:
|
||||
m = self.generator.run()
|
||||
self.assertTrue(self.is_connex(m.tiles))
|
||||
|
||||
def test_loops(self) -> None:
|
||||
m = self.stom(3 * ".. ..\n")
|
||||
self.generator.add_loop(m.tiles, 1, 3)
|
||||
s = self.mtos(m)
|
||||
self.assertEqual(s, ".. ..\n.......\n.. ..")
|
||||
self.assertFalse(self.generator.add_loop(m.tiles, 0, 0))
|
||||
m = self.stom("...\n. .\n...")
|
||||
self.assertFalse(self.generator.add_loop(m.tiles, 1, 1))
|
@ -1,3 +1,9 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class FakePad:
|
||||
"""
|
||||
In order to run tests, we simulate a fake curses pad that accepts functions
|
||||
@ -6,12 +12,18 @@ class FakePad:
|
||||
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
|
||||
pass
|
||||
|
||||
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
|
||||
smincol: int, smaxrow: int, smaxcol: int) -> None:
|
||||
def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int,
|
||||
smincol: int, smaxrow: int, smaxcol: int) -> None:
|
||||
pass
|
||||
|
||||
def clear(self) -> None:
|
||||
def erase(self) -> None:
|
||||
pass
|
||||
|
||||
def resize(self, height: int, width: int) -> None:
|
||||
pass
|
||||
|
||||
def getmaxyx(self) -> Tuple[int, int]:
|
||||
return 42, 42
|
||||
|
||||
def inch(self, y: int, x: int) -> str:
|
||||
return "i"
|
||||
|
@ -1,12 +1,19 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.settings import Settings
|
||||
from squirrelbattle.translations import Translator
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
Translator.setlocale("en")
|
||||
|
||||
def test_settings(self) -> None:
|
||||
"""
|
||||
Ensure that settings are well loaded.
|
||||
Ensures that settings are well loaded.
|
||||
"""
|
||||
settings = Settings()
|
||||
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')
|
||||
@ -21,7 +28,7 @@ class TestSettings(unittest.TestCase):
|
||||
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
|
||||
settings.get_comment('TEXTURE_PACK'))
|
||||
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
|
||||
'Pack de textures utilisé')
|
||||
'Texture pack')
|
||||
|
||||
settings.TEXTURE_PACK = 'squirrel'
|
||||
self.assertEqual(settings.TEXTURE_PACK, 'squirrel')
|
||||
|
93
squirrelbattle/tests/translations_test.py
Normal file
93
squirrelbattle/tests/translations_test.py
Normal file
@ -0,0 +1,93 @@
|
||||
import unittest
|
||||
|
||||
from squirrelbattle.translations import gettext as _, Translator
|
||||
|
||||
|
||||
class TestTranslations(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
Translator.compilemessages()
|
||||
Translator.refresh_translations()
|
||||
Translator.setlocale("fr")
|
||||
|
||||
def test_main_menu_translation(self) -> None:
|
||||
"""
|
||||
Ensures that the main menu is translated.
|
||||
"""
|
||||
self.assertEqual(_("New game"), "Nouvelle partie")
|
||||
self.assertEqual(_("Resume"), "Continuer")
|
||||
self.assertEqual(_("Load"), "Charger")
|
||||
self.assertEqual(_("Save"), "Sauvegarder")
|
||||
self.assertEqual(_("Settings"), "Paramètres")
|
||||
self.assertEqual(_("Exit"), "Quitter")
|
||||
|
||||
def test_settings_menu_translation(self) -> None:
|
||||
"""
|
||||
Ensures that the settings menu is translated.
|
||||
"""
|
||||
self.assertEqual(_("Main key to move up"),
|
||||
"Touche principale pour aller vers le haut")
|
||||
self.assertEqual(_("Secondary key to move up"),
|
||||
"Touche secondaire pour aller vers le haut")
|
||||
self.assertEqual(_("Main key to move down"),
|
||||
"Touche principale pour aller vers le bas")
|
||||
self.assertEqual(_("Secondary key to move down"),
|
||||
"Touche secondaire pour aller vers le bas")
|
||||
self.assertEqual(_("Main key to move left"),
|
||||
"Touche principale pour aller vers la gauche")
|
||||
self.assertEqual(_("Secondary key to move left"),
|
||||
"Touche secondaire pour aller vers la gauche")
|
||||
self.assertEqual(_("Main key to move right"),
|
||||
"Touche principale pour aller vers la droite")
|
||||
self.assertEqual(_("Secondary key to move right"),
|
||||
"Touche secondaire pour aller vers la droite")
|
||||
self.assertEqual(_("Key to validate a menu"),
|
||||
"Touche pour valider un menu")
|
||||
self.assertEqual(_("Key used to open the inventory"),
|
||||
"Touche utilisée pour ouvrir l'inventaire")
|
||||
self.assertEqual(_("Key used to use an item in the inventory"),
|
||||
"Touche pour utiliser un objet de l'inventaire")
|
||||
self.assertEqual(_("Key used to equip an item in the inventory"),
|
||||
"Touche pour équiper un objet de l'inventaire")
|
||||
self.assertEqual(_("Key used to drop an item in the inventory"),
|
||||
"Touche pour jeter un objet de l'inventaire")
|
||||
self.assertEqual(_("Key used to talk to a friendly entity"),
|
||||
"Touche pour parler à une entité pacifique")
|
||||
self.assertEqual(_("Key used to wait"), "Touche pour attendre")
|
||||
self.assertEqual(_("Key used to use ladders"),
|
||||
"Touche pour utiliser les échelles")
|
||||
self.assertEqual(_("Key used to use a bow"),
|
||||
"Touche pour utiliser un arc")
|
||||
self.assertEqual(_("Key used to dance"),
|
||||
"Touche pour danser")
|
||||
self.assertEqual(_("Texture pack"), "Pack de textures")
|
||||
self.assertEqual(_("Language"), "Langue")
|
||||
|
||||
def test_entities_translation(self) -> None:
|
||||
self.assertEqual(_("player"), "joueur")
|
||||
|
||||
self.assertEqual(_("hedgehog"), "hérisson")
|
||||
self.assertEqual(_("merchant"), "marchand")
|
||||
self.assertEqual(_("rabbit"), "lapin")
|
||||
self.assertEqual(_("sunflower"), "tournesol")
|
||||
self.assertEqual(_("teddy bear"), "nounours")
|
||||
self.assertEqual(_("tiger"), "tigre")
|
||||
self.assertEqual(_("eagle"), "pygargue")
|
||||
|
||||
self.assertEqual(_("body snatch potion"), "potion d'arrachage de corps")
|
||||
self.assertEqual(_("bomb"), "bombe")
|
||||
self.assertEqual(_("explosion"), "explosion")
|
||||
self.assertEqual(_("heart"), "cœur")
|
||||
self.assertEqual(_("sword"), "épée")
|
||||
self.assertEqual(_("helmet"), "casque")
|
||||
self.assertEqual(_("chestplate"), "plastron")
|
||||
self.assertEqual(_("shield"), "bouclier")
|
||||
self.assertEqual(_("ruler"), "règle")
|
||||
self.assertEqual(_("scroll of damage"), "parchemin de dégâts")
|
||||
self.assertEqual(_("scroll of weakness"), "parchemin de faiblesse")
|
||||
self.assertEqual(_("bow"), "arc")
|
||||
self.assertEqual(_("fire ball staff"), "baton de boule de feu")
|
||||
self.assertEqual(_("ring of critical damage"),
|
||||
"anneau de coup critique")
|
||||
self.assertEqual(_("ring of more experience"),
|
||||
"anneau de plus d'expérience")
|
||||
self.assertEqual(_("monocle"), "monocle")
|
109
squirrelbattle/translations.py
Normal file
109
squirrelbattle/translations.py
Normal file
@ -0,0 +1,109 @@
|
||||
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import gettext as gt
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any, List
|
||||
|
||||
|
||||
class Translator:
|
||||
"""
|
||||
This module uses gettext to translate strings.
|
||||
Translator.setlocale defines the language of the strings,
|
||||
then gettext() translates the messages.
|
||||
"""
|
||||
SUPPORTED_LOCALES: List[str] = ["de", "en", "es", "fr"]
|
||||
locale: str = "en"
|
||||
translators: dict = {}
|
||||
|
||||
@classmethod
|
||||
def refresh_translations(cls) -> None:
|
||||
"""
|
||||
Loads compiled translations.
|
||||
"""
|
||||
for language in cls.SUPPORTED_LOCALES:
|
||||
if language == "en":
|
||||
continue
|
||||
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
|
||||
rep.mkdir(parents=True) if not rep.is_dir() else None
|
||||
if os.path.isfile(rep / "squirrelbattle.mo"):
|
||||
cls.translators[language] = gt.translation(
|
||||
"squirrelbattle",
|
||||
localedir=Path(__file__).parent / "locale",
|
||||
languages=[language],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setlocale(cls, lang: str) -> None:
|
||||
"""
|
||||
Defines the language used to translate the game.
|
||||
The language must be supported, otherwise nothing is done.
|
||||
"""
|
||||
lang = lang[:2]
|
||||
if lang in cls.SUPPORTED_LOCALES:
|
||||
cls.locale = lang
|
||||
|
||||
@classmethod
|
||||
def get_translator(cls) -> Any:
|
||||
return cls.translators.get(cls.locale, gt.NullTranslations())
|
||||
|
||||
@classmethod
|
||||
def makemessages(cls) -> None: # pragma: no cover
|
||||
"""
|
||||
Analyses all strings in the project and extracts them.
|
||||
"""
|
||||
for language in cls.SUPPORTED_LOCALES:
|
||||
if language == "en":
|
||||
# Don't translate the main language
|
||||
continue
|
||||
file_name = Path(__file__).parent / "locale" / language \
|
||||
/ "LC_MESSAGES" / "squirrelbattle.po"
|
||||
args = ["find", "squirrelbattle", "-iname", "*.py"]
|
||||
find = subprocess.Popen(args, cwd=Path(__file__).parent.parent,
|
||||
stdout=subprocess.PIPE)
|
||||
args = ["xargs", "xgettext", "--from-code", "utf-8",
|
||||
"--add-comments",
|
||||
"--package-name=squirrelbattle",
|
||||
"--package-version=23.14",
|
||||
"--copyright-holder=ÿnérant, eichhornchen, "
|
||||
"nicomarg, charlse, ifugao",
|
||||
"--msgid-bugs-address=squirrel-battle@crans.org",
|
||||
"--sort-by-file",
|
||||
"-o", file_name]
|
||||
if file_name.is_file():
|
||||
args.append("--join-existing")
|
||||
with open(file_name, "r") as f:
|
||||
content = f.read()
|
||||
with open(file_name, "w") as f:
|
||||
f.write(re.sub("#:.*\n", "", content))
|
||||
print(f"Make {language} messages...")
|
||||
subprocess.Popen(args, stdin=find.stdout).wait()
|
||||
|
||||
@classmethod
|
||||
def compilemessages(cls) -> None:
|
||||
"""
|
||||
Compiles translation messages from source files.
|
||||
"""
|
||||
for language in cls.SUPPORTED_LOCALES:
|
||||
if language == "en":
|
||||
continue
|
||||
args = ["msgfmt", "--check-format",
|
||||
"-o", Path(__file__).parent / "locale" / language
|
||||
/ "LC_MESSAGES" / "squirrelbattle.mo",
|
||||
Path(__file__).parent / "locale" / language
|
||||
/ "LC_MESSAGES" / "squirrelbattle.po"]
|
||||
print(f"Compiling {language} messages...")
|
||||
subprocess.Popen(args).wait()
|
||||
|
||||
|
||||
def gettext(message: str) -> str:
|
||||
"""
|
||||
Translates a message.
|
||||
"""
|
||||
return Translator.get_translator().gettext(message)
|
||||
|
||||
|
||||
Translator.refresh_translations()
|
Loading…
x
Reference in New Issue
Block a user