Compare commits

...

419 Commits

Author SHA1 Message Date
ynerant 4c274bebb9 Merge branch 'md' into 'master'
Fix Readme

See merge request ynerant/squirrel-battle!82
2021-01-25 15:04:47 +01:00
ynerant a28909bb70 Fix Readme 2021-01-25 15:04:47 +01:00
ynerant 6566f5140a Merge branch 'fix-ladders' into 'master'
Fix ladders

Closes #81

See merge request ynerant/squirrel-battle!81
2021-01-25 14:38:58 +01:00
Yohann D'ANELLO 776f8ed88c
Fixes #81 2021-01-25 14:31:37 +01:00
ynerant 284a22c92e Merge branch 'fix-yay' into 'master'
Don't create an english translation file

See merge request ynerant/squirrel-battle!80
2021-01-21 01:14:17 +01:00
Yohann D'ANELLO 3d019d3ca8
Don't create an english translation file 2021-01-21 01:12:55 +01:00
ynerant 99b749aaa2 Merge branch 'fix-explosions' into 'master'
Fix explosions

Closes #80

See merge request ynerant/squirrel-battle!79
2021-01-16 00:46:04 +01:00
Yohann D'ANELLO d978d319bc
Entities are living during two ticks, fixes #80 2021-01-16 00:40:32 +01:00
Yohann D'ANELLO 87e896bd06
Item owners are correctly set 2021-01-16 00:26:33 +01:00
ynerant e233243b81 Merge branch 'es-translation' into 'master'
Fix spanish translation

See merge request ynerant/squirrel-battle!78
2021-01-11 01:48:10 +01:00
Yohann D'ANELLO 7ce3b8cd5d
Fix spanish translation 2021-01-11 01:40:55 +01:00
ynerant fa0a0a79ea Merge branch 'map_generation' into 'master'
Add new room type : chunk rooms

See merge request ynerant/squirrel-battle!77
2021-01-11 01:28:54 +01:00
Yohann D'ANELLO d839356b2a
Merge remote-tracking branch 'origin/map_generation' into map_generation
# Conflicts:
#	squirrelbattle/mapgeneration/broguelike.py
2021-01-11 01:23:26 +01:00
Yohann D'ANELLO 7b019ce149
Linting 2021-01-11 01:21:52 +01:00
Charles Peyrat 03c45a970c
Fix merging mistakes and chunk rooms 2021-01-11 01:19:54 +01:00
Charles Peyrat 79d8ef3a44
Add new room type : chunk rooms 2021-01-11 01:19:54 +01:00
Charles Peyrat c854d41579 Fix merging mistakes and chunk rooms 2021-01-11 01:18:20 +01:00
ynerant 6294f9c07f Merge branch 'v23.14' into 'master'
Bump to version 23.14

See merge request ynerant/squirrel-battle!76
2021-01-10 23:58:31 +01:00
Yohann D'ANELLO 57605c969f
Bump to version 23.14 2021-01-10 23:57:39 +01:00
ynerant e9374c5e6b Merge branch 'map_generation' into 'master'
Map generation

Closes #5

See merge request ynerant/squirrel-battle!35
2021-01-10 23:54:28 +01:00
ynerant b72e41d14d Merge branch 'doors' into 'map_generation'
Doors

See merge request ynerant/squirrel-battle!75
2021-01-10 23:54:13 +01:00
Yohann D'ANELLO 588357e5bf
Linting 2021-01-10 23:49:43 +01:00
ynerant 2031d7fa67 Merge branch 'map_generation' into 'doors'
# Conflicts:
#   squirrelbattle/mapgeneration/broguelike.py
2021-01-10 23:43:54 +01:00
Yohann D'ANELLO 65ae99a26d
The logs of the map was not updated 2021-01-10 23:41:51 +01:00
Charles Peyrat 5aaef15b2b Add new room type : chunk rooms 2021-01-10 23:38:29 +01:00
Yohann D'ANELLO 60675d7859
Cover doors code 2021-01-10 23:21:28 +01:00
Yohann D'ANELLO b0ca1d4edf
Cover everytime the map generation test 2021-01-10 23:05:49 +01:00
Yohann D'ANELLO 8f845d1e4c
Doors don't break the connexity of map 2021-01-10 23:03:24 +01:00
Charles Peyrat ca67d5d7f4 Merge remote branch 2021-01-10 23:02:34 +01:00
Yohann D'ANELLO 11daa8573c
The players can open doors 2021-01-10 22:59:34 +01:00
Charles Peyrat 099a0eab31 Add comments and docstring 2021-01-10 22:54:48 +01:00
Yohann D'ANELLO 6c0aaffd77
Doors are walls 2021-01-10 22:53:27 +01:00
Yohann D'ANELLO e744310861
Place doors at the beginning of the corridor 2021-01-10 22:51:01 +01:00
nicomarg 96e9612d16 Merge branch 'equipment-in-inventory' into 'master'
Resolve "Display equipment in inventory"

Closes #75

See merge request ynerant/squirrel-battle!71
2021-01-10 22:45:04 +01:00
Nicolas Margulies f05652d9b8 Fixed tests and reached 100% coverage 2021-01-10 22:39:52 +01:00
Yohann D'ANELLO 5e378fc2d0
Update game rules 2021-01-10 22:27:46 +01:00
Nicolas Margulies 519504fc32 Mark equipped items and allow unequipping 2021-01-10 22:26:43 +01:00
Yohann D'ANELLO 0ea10546ac
Fix merge issues 2021-01-10 22:19:15 +01:00
Yohann D'ANELLO 4c7a2e9f3b
Merge branch 'master' into map_generation
# Conflicts:
#	squirrelbattle/entities/player.py
#	squirrelbattle/game.py
#	squirrelbattle/interfaces.py
#	squirrelbattle/tests/game_test.py
2021-01-10 22:16:11 +01:00
Nicolas Margulies 88471f4361 Changed equipment behaviour, now equipped items stay in the inventory 2021-01-10 22:15:32 +01:00
Yohann D'ANELLO 9df1ac7883
Linting 2021-01-10 22:08:42 +01:00
Yohann D'ANELLO 01cdea6edc
Don't spawn random entities on each level anymore 2021-01-10 21:57:51 +01:00
eichhornchen b004dec8c9 Merge branch 'translate' into 'master'
Translate

See merge request ynerant/squirrel-battle!73
2021-01-10 21:52:39 +01:00
Charles Peyrat 13e3628668 Merge branch 'map_generation' of https://gitlab.crans.org/ynerant/squirrel-battle into map_generation 2021-01-10 21:50:01 +01:00
Charles Peyrat 12e19759aa Implement populate method, so map generation also handles entity spawn 2021-01-10 21:49:39 +01:00
Yohann D'ANELLO d3607248c0
Remove unused translations 2021-01-10 21:48:46 +01:00
Charles Peyrat e639ad6255 Getting to full cover, and minor fix to bug that allowed corridors to create loops in a room, resulting in implacability 2021-01-10 21:48:12 +01:00
eichhornchen 236481ae1c linting 2021-01-10 21:44:45 +01:00
eichhornchen 18ace5144c Repaired a bug : a variable was not declared before it was used in interfaces.py take_damage 2021-01-10 21:41:45 +01:00
eichhornchen 6cf0590586 even more translations. 2021-01-10 21:38:21 +01:00
Charles Peyrat 96bbc5088f Add a test case for non connex maps in distance computation 2021-01-10 21:32:58 +01:00
Charles Peyrat 26e66a5796 Implement method add_loops along with tests 2021-01-10 21:30:18 +01:00
eichhornchen 4f4223c514 More translations. 2021-01-10 21:25:15 +01:00
eichhornchen 893a23f95c Repaired a but : bears dealt 1 damage instead of 0. 2021-01-10 21:15:42 +01:00
eichhornchen a1d69203c9 Translations. 2021-01-10 21:15:30 +01:00
ynerant b6a448b39b Merge branch 'fix-load' into 'master'
Fix load

Closes #73 et #76

See merge request ynerant/squirrel-battle!72
2021-01-10 21:05:13 +01:00
Yohann D'ANELLO d2af345c0c
Fix linting 2021-01-10 21:01:43 +01:00
Yohann D'ANELLO 67a9bda6e1
Don't fill the inventory of a chest or a Merchant if it is empty, fixes #73 2021-01-10 20:46:01 +01:00
Yohann D'ANELLO 3758cb1336 Fix the GiantSeaEagle registration, fixes #76 2021-01-10 20:20:12 +01:00
ynerant 44b967625c Merge branch 'fix-player-inventory' into 'master'
Update the player inventory when opening the menu/loading the game, fixes #72

Closes #72

See merge request ynerant/squirrel-battle!70
2021-01-10 19:57:17 +01:00
Yohann D'ANELLO 5694dd4dff Update the player inventory when opening the menu/loading the game, fixes #72 2021-01-10 19:52:28 +01:00
nicomarg 2efa9d4575 Merge branch 'fix-mobs-vision-bug' into 'master'
Resolve "Fix mobs vision bug"

Closes #71

See merge request ynerant/squirrel-battle!69
2021-01-10 19:48:01 +01:00
Nicolas Margulies 16ddbd31f3 also prevent updating seen tiles when checking vision 2021-01-10 19:41:51 +01:00
ynerant 0821a5fa7a Merge branch 'broken-bows' into 'master'
Broken bows

Closes #70

See merge request ynerant/squirrel-battle!68
2021-01-10 19:30:46 +01:00
Yohann D'ANELLO 01ee49ddd4 Entities are updated after a bow shot, fixes #70 2021-01-10 19:25:12 +01:00
Yohann D'ANELLO cddff5c2d9 Fix linting 2021-01-10 19:17:04 +01:00
eichhornchen f4965039ee Merge branch 'EvenmoreDoc' into 'master'
Evenmore doc

See merge request ynerant/squirrel-battle!67
2021-01-10 19:03:47 +01:00
ynerant ce1d299de7 Merge branch 'moredocs' into 'EvenmoreDoc'
More docs

See merge request ynerant/squirrel-battle!66
2021-01-10 18:56:15 +01:00
eichhornchen 2b63f8b3f1 Added stats in doc. 2021-01-10 18:54:36 +01:00
ynerant 10a1b59b24 Merge branch 'EvenmoreDoc' into 'moredocs'
# Conflicts:
#   docs/entities/items.rst
2021-01-10 18:49:25 +01:00
eichhornchen c9994423cb Added dance to doc. 2021-01-10 18:44:17 +01:00
Yohann D'ANELLO bc638b3bec Monocles can be found in chests 2021-01-10 18:42:26 +01:00
eichhornchen 258bd0d816 re-itme doc 2021-01-10 18:38:56 +01:00
eichhornchen e96c50e81a Doc for new items. 2021-01-10 18:36:46 +01:00
Yohann D'ANELLO 82421be8bb Documentation on monocles 2021-01-10 18:16:01 +01:00
Yohann D'ANELLO 1795d62294 Update merchant prices 2021-01-10 18:09:09 +01:00
eichhornchen 0de2df0bd2 Merge branch 'levelup' into 'master'
Levelup

Closes #69

See merge request ynerant/squirrel-battle!65
2021-01-10 18:08:27 +01:00
eichhornchen 93e51d9240 Testing + linting (yes there remains two linting errors, i don't know what to do. 2021-01-10 18:04:33 +01:00
Yohann D'ANELLO 568b8f3eba Documentation on displays 2021-01-10 17:33:06 +01:00
eichhornchen afaa12d86b Updated doc and README conderning dancing 2021-01-10 17:13:07 +01:00
eichhornchen 3d48c43886 Player can now dance! Closes #69. 2021-01-10 17:10:00 +01:00
eichhornchen dfb591d410 The player's stats now get better when levelling up. The strength level and frequency of appearance of entities have been changed to offer bettter game experience. 2021-01-10 16:31:46 +01:00
eichhornchen 841c7b9f90 Chest can be destroyed by bombs. 2021-01-10 16:31:16 +01:00
eichhornchen e56fc502cb Merge branch 'clean' into 'master'
Clean

See merge request ynerant/squirrel-battle!64
2021-01-10 15:49:00 +01:00
eichhornchen 97ecd13c77 Readme got even better ! 2021-01-10 15:45:26 +01:00
eichhornchen 9516230893 We now have a useful Readme with a getting started manual. 2021-01-10 15:38:39 +01:00
eichhornchen 5eb7699301 Rearranged the display class files, related to issue #56. 2021-01-10 12:35:50 +01:00
eichhornchen 8d0d0883a3 Merge branch 'master' into clean 2021-01-10 12:12:40 +01:00
ynerant 5c07071cff Merge branch 'moredocs' into 'master'
Moredocs

See merge request ynerant/squirrel-battle!63
2021-01-10 12:11:37 +01:00
Yohann D'ANELLO 0ef5875a72
Documentation on ladders 2021-01-10 12:07:53 +01:00
Yohann D'ANELLO 92a3081b2e
Display "Edit on Gitlab" button 2021-01-10 11:58:32 +01:00
eichhornchen dd37c2f62f Renamed the title's ascii art file. 2021-01-10 11:54:49 +01:00
Yohann D'ANELLO 6d786c7358
Documentation on unit tests 2021-01-10 11:30:04 +01:00
Yohann D'ANELLO d738029335
Fix import order 2021-01-10 11:25:53 +01:00
Yohann D'ANELLO 94c12541ef
Happy new year! 2021-01-10 10:46:17 +01:00
Yohann D'ANELLO 391fe6897b
Drop unusued english translation file 2021-01-10 10:29:43 +01:00
ynerant 5b5180d44a Merge branch 'fix-tty-colors' into 'master'
Don't use custom colors on unsupported screens, fixes #67

Closes #67

See merge request ynerant/squirrel-battle!61
2021-01-09 21:52:03 +01:00
nicomarg 1e5a268586 Merge branch 'remove-debug-code' into 'master'
Removed debug code

See merge request ynerant/squirrel-battle!62
2021-01-09 18:49:20 +01:00
Nicolas Margulies 7bf1789239 Removed debug code 2021-01-09 18:45:55 +01:00
Yohann D'ANELLO 8cb2b2388f
Don't use custom colors on unsupported screens, fixes #67 2021-01-09 18:42:11 +01:00
eichhornchen 93a9e5e4c4 Merge branch 'moreitems' into 'master'
Moreitems

Closes #64 and #60

See merge request ynerant/squirrel-battle!60
2021-01-09 00:05:14 +01:00
eichhornchen ac4a73b2cb Final linting 2021-01-09 00:01:02 +01:00
eichhornchen cbe3e226b4 Repaired a merge error, tests now work. 2021-01-08 23:51:47 +01:00
eichhornchen eac9057f31 Merge branch 'master' into 'moreitems'
# Conflicts:
#   squirrelbattle/entities/items.py
#   squirrelbattle/interfaces.py
#   squirrelbattle/tests/game_test.py
2021-01-08 23:41:21 +01:00
eichhornchen 2eb42668c8 Linting and tests for chests 2021-01-08 23:32:47 +01:00
eichhornchen bdbf214d8d Added chests, they are immortal and contain objects the player can take for free. 2021-01-08 23:15:48 +01:00
eichhornchen 175706b1e4 Merchants had default maxhealth. 2021-01-08 22:30:30 +01:00
eichhornchen 9ff615a6b0 Linting and tests... 2021-01-08 22:25:00 +01:00
Yohann D'ANELLO ad3cce116e
Load map floor index when loading a new game 2021-01-08 21:23:12 +01:00
Yohann D'ANELLO 949555ffff
Map at floor -1 is now not deterministic 2021-01-08 20:06:32 +01:00
Yohann D'ANELLO 571857b063
Generate a random map when changing floor 2021-01-08 20:00:42 +01:00
Yohann D'ANELLO d8d0bc6190
Fix the end ladder 2021-01-08 20:00:10 +01:00
Charles Peyrat 9e099d0715 Ladders should spawn with no wall nearby 2021-01-08 19:50:27 +01:00
eichhornchen 746379bad6 Now with EXPLOSIONS! 2021-01-08 19:18:29 +01:00
eichhornchen 591630b8a7 Added a fire ball staff, closes #64 2021-01-08 19:05:02 +01:00
eichhornchen 903a06c36c Subclassed and removed some debugging code 2021-01-08 18:38:54 +01:00
eichhornchen f6210a6356 Added a Bow, related to #64 2021-01-08 18:06:26 +01:00
nicomarg da8cf238ee Merge branch 'mobs-seeing-through-walls' into 'master'
Resolve "Mobs seeing through walls"

Closes #63

See merge request ynerant/squirrel-battle!59
2021-01-08 17:34:27 +01:00
Nicolas Margulies 46a5dc6931 Made mobs check if they can see the player 2021-01-08 17:26:56 +01:00
Charles Peyrat 7e14122b8c Randomly place exit ladder 2021-01-08 17:25:52 +01:00
Yohann D'ANELLO df2c1a4b55
Add ladder on the start position 2021-01-08 17:10:42 +01:00
Yohann D'ANELLO 8e7029e34d
Fix walls 2021-01-08 17:10:30 +01:00
Yohann D'ANELLO 1ab63434f6
Merge branch 'master' into map_generation
# Conflicts:
#	squirrelbattle/game.py
#	squirrelbattle/interfaces.py
#	squirrelbattle/tests/game_test.py
2021-01-08 17:02:10 +01:00
Yohann D'ANELLO afaa9d17cd
Linting 2021-01-08 16:55:02 +01:00
Yohann D'ANELLO 9b853324ad
Drop first version of random walk 2021-01-08 16:52:38 +01:00
Charles Peyrat c216a6089e Add a break so that generated rooms arre only placed once 2021-01-08 16:51:04 +01:00
Charles Peyrat a390f4f5e9 Fix is_connex tests 2021-01-08 16:21:16 +01:00
eichhornchen bde33e9232 Added a second scroll object closes #60 2021-01-08 16:14:40 +01:00
Charles Peyrat 0aa4eb9c0b Refactoring in tests to allow for easy connexity verification 2021-01-08 16:11:17 +01:00
ynerant 4a80dc36ad Merge branch 'save_floors' into 'master'
Save floors

Closes #62 et #61

See merge request ynerant/squirrel-battle!58
2021-01-08 16:05:10 +01:00
Yohann D'ANELLO 7a4936e6a5
Test monocles 2021-01-08 15:58:54 +01:00
Charles Peyrat 785ac403e3 Forbid walker from ever reaching the outer most edge of the map 2021-01-08 15:56:30 +01:00
Charles Peyrat f240cafa83 Fixing syntax in tests 2021-01-08 15:55:26 +01:00
Yohann D'ANELLO a497d08f31
Implement the monocle of truth, closes #62 2021-01-08 15:48:12 +01:00
Charles Peyrat 5424c7cd98 Nicer default parameters 2021-01-08 15:20:32 +01:00
Charles Peyrat dab84738d9 Remove the starting room door only if it really shouldn't be here; also account for the new randomized placement in removing lone door tile 2021-01-08 15:18:13 +01:00
Yohann D'ANELLO 1270640619
Change hint whenever the T key is pressed or not 2021-01-08 15:07:35 +01:00
Charles Peyrat 8d7e264381 Fix a bug where the generator could crash by trying to place the starting room out of bounds; starting room position is now random 2021-01-08 15:06:38 +01:00
Yohann D'ANELLO 28a6532a21
Add a hint to tell the user how to talk to friendly entities 2021-01-08 14:59:44 +01:00
Charles Peyrat fab1bee8d8 Force loop entrance to get coverage 2021-01-08 14:52:59 +01:00
Yohann D'ANELLO 156e4a7e3a
Add a hint to tell the user how to use ladders 2021-01-08 14:51:56 +01:00
Yohann D'ANELLO f48377e055
Save floors and visibility, fixes #61 2021-01-08 14:23:57 +01:00
ynerant 073e3d3740 Merge branch 'description' into 'master'
Description

Closes #59

See merge request ynerant/squirrel-battle!57
2021-01-08 12:10:29 +01:00
Yohann D'ANELLO 120ec82d09
Helmet needs only one character in squirrel mode, add a trailing space 2021-01-08 12:07:13 +01:00
Yohann D'ANELLO 571e9db3e8
Fix french translations 2021-01-08 12:06:28 +01:00
Yohann D'ANELLO e56bdc16c2
Add item description, closes #59 2021-01-08 11:55:25 +01:00
eichhornchen 5736c2300b Added a scroll object that deals damage based on the player intelligence. Related to #60 2021-01-08 11:54:39 +01:00
ynerant 2b3a8279b8 Merge branch 'fix-settings' into 'master'
Fix settings

Closes #57 et #58

See merge request ynerant/squirrel-battle!56
2021-01-08 11:30:02 +01:00
Yohann D'ANELLO 75e93611c3
Capture all mouse events and take into account mouse attributes, fixes #58 2021-01-08 11:21:40 +01:00
eichhornchen ea58d5b426 Merge branch 'doc' into 'master'
Doc

Closes #53

See merge request ynerant/squirrel-battle!55
2021-01-08 11:15:42 +01:00
Yohann D'ANELLO b42f1277b1
Exit the game on KeyboardInterrupt (don't log this error) 2021-01-08 11:10:10 +01:00
eichhornchen 196e3708d2 Merge branch 'master' into 'doc'
# Conflicts:
#   squirrelbattle/entities/items.py
#   squirrelbattle/interfaces.py
2021-01-08 11:10:09 +01:00
Yohann D'ANELLO ac8c7a0a4c
Only read required keys in settings file 2021-01-08 11:07:38 +01:00
eichhornchen 6673b67ffe Repaired spawn of trumpets. 2021-01-08 10:58:40 +01:00
Charles Peyrat c06f903a16 Fix a typo that made corridors unable to be built 2021-01-08 07:41:00 +01:00
Charles Peyrat c6947fab44 Integrate the new map generation into the game ! Closes #5 2021-01-08 07:39:52 +01:00
Charles Peyrat 641f5c7872 Make generation more sparse by asking for extra space around rooms; also add out of bounds option to Map.neighbourhood 2021-01-08 07:38:47 +01:00
Charles Peyrat 605696dddd Revamp door placing algorithm so that it generates cleaner doors; also remove lone starting room door from level 2021-01-08 07:36:31 +01:00
Charles Peyrat 5ba07afc9f Fix typo in parameter names 2021-01-08 07:05:02 +01:00
Charles Peyrat e21d4d230c Add missing termination condition 2021-01-08 07:04:24 +01:00
Charles Peyrat b0ac580677 Fix place_walls, that placed floors instead ... 2021-01-08 07:03:49 +01:00
Charles Peyrat d362bdc949 Fix place_room and add missing argument 2021-01-08 06:58:02 +01:00
Charles Peyrat 9c252a2bbc Correct out of bounds errors and add missing arguments to range call 2021-01-08 06:54:01 +01:00
Charles Peyrat c959a9d865 Update tests because Map.neighbourhood became a static method 2021-01-08 05:42:12 +01:00
Charles Peyrat 8475e5228e Large neighbourhood shouldn't return the central cell 2021-01-08 05:41:16 +01:00
Charles Peyrat 20cbf546f9 Correct formulas for random enumeration of a grid 2021-01-08 05:21:31 +01:00
Charles Peyrat 49e261557c Fix typos 2021-01-08 05:14:46 +01:00
Charles Peyrat abbad0f352 Fix formulas in place_room and room_fits 2021-01-08 05:14:32 +01:00
Charles Peyrat 05ccd0e339 Circular rooms should not try to generate any holes if their radius isn't large enough 2021-01-08 04:51:20 +01:00
Charles Peyrat c6f66d95f2 Fix typos 2021-01-08 04:48:32 +01:00
Charles Peyrat 6fbc757f1e Implement method place_walls 2021-01-08 04:43:10 +01:00
Charles Peyrat ffa7641b21 Made Map.neighbourhood a static method 2021-01-08 04:36:57 +01:00
Charles Peyrat 3229eb8ea7 Implement place_room method 2021-01-08 03:45:26 +01:00
Charles Peyrat 42f0c195aa Add prototype for create_random_room; change return value of attach_doors and create_circular_room so we have info on door direction; minor syntax change 2021-01-08 03:43:20 +01:00
Charles Peyrat ddbd0299a0 Implement method room_fits 2021-01-08 03:38:37 +01:00
Charles Peyrat 5cbf15bef5 Return value of Generator.run should be a Map 2021-01-08 03:37:10 +01:00
Charles Peyrat bb3422f7d8 Add main generation loop 2021-01-08 03:19:59 +01:00
ynerant fb47c15d6b Merge branch 'equipment' into 'master'
Equipment

Closes #19, #30, #48, #51 et #52

See merge request ynerant/squirrel-battle!54
2021-01-08 02:19:29 +01:00
Yohann D'ANELLO affc1bae59
Fix merge 2021-01-08 02:15:13 +01:00
ynerant 451dd9cba7 Merge branch 'master' into 'equipment'
# Conflicts:
#   squirrelbattle/display/statsdisplay.py
#   squirrelbattle/entities/items.py
#   squirrelbattle/entities/player.py
#   squirrelbattle/interfaces.py
#   squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
#   squirrelbattle/tests/game_test.py
2021-01-08 02:11:40 +01:00
ynerant da47731faf Merge branch 'lighting' into 'master'
Lighting

Closes #27

See merge request ynerant/squirrel-battle!34
2021-01-08 02:01:13 +01:00
Yohann D'ANELLO 7aeb659cf5
Fix french translation 2021-01-08 02:00:22 +01:00
Yohann D'ANELLO 6c6a44fb18
More tests 2021-01-08 01:56:54 +01:00
Yohann D'ANELLO 478a655751
Fix fg/bg custom colors 2021-01-07 16:49:40 +01:00
ynerant 4acf6804d4 Merge branch 'master' into 'lighting'
# Conflicts:
#   squirrelbattle/display/mapdisplay.py
#   squirrelbattle/interfaces.py
2021-01-07 16:36:54 +01:00
Yohann D'ANELLO c36e68d6e4
Reduce player vision 2021-01-07 16:34:12 +01:00
Yohann D'ANELLO e9c8f43e7e
Use ternary conditions to add coverage 2021-01-07 16:31:39 +01:00
Charles Peyrat 5579f5d791 Room now can now generate with a corridor; implemenent door placement finding 2021-01-07 07:06:08 +01:00
Charles Peyrat 9fb366aaab Make name follow style convention 2021-01-07 05:02:49 +01:00
Yohann D'ANELLO 1a78ad584c
Move equip functions for items 2021-01-06 18:31:28 +01:00
Yohann D'ANELLO a8c0c197ed
Linting 2021-01-06 18:02:58 +01:00
ynerant 00a4dec6a4 Merge branch 'ladders' into 'master'
Ladders

See merge request ynerant/squirrel-battle!53
2021-01-06 18:01:36 +01:00
Eichhornchen 5ef12bef3d Changed the prices of armor 2021-01-06 17:57:23 +01:00
Yohann D'ANELLO 093c105120
The broken test is mysteriously working now 2021-01-06 17:54:43 +01:00
Eichhornchen b403bc4784 documentation 2021-01-06 17:48:03 +01:00
Yohann D'ANELLO ae505166b7
Disable critical hits during tests 2021-01-06 17:39:13 +01:00
ynerant d49c138257 Merge branch 'master' into 'ladders'
# Conflicts:
#   squirrelbattle/game.py
#   squirrelbattle/interfaces.py
#   squirrelbattle/tests/game_test.py
2021-01-06 17:29:26 +01:00
Yohann D'ANELLO 0c2b10b031
Use ternary conditions to gain coverage 2021-01-06 17:21:17 +01:00
Yohann D'ANELLO 887a190f11
Less complexity on the handle key function 2021-01-06 17:00:43 +01:00
Yohann D'ANELLO 41548504de
Test ladders 2021-01-06 16:09:53 +01:00
Yohann D'ANELLO a48e6325fe
Add log message when the player switches floor 2021-01-06 15:55:44 +01:00
Eichhornchen 15ef6af998 Put the texturepack in alphabetical order again. 2021-01-06 15:49:54 +01:00
Yohann D'ANELLO 4cd4bc9005
Display the current floor in the StatsDisplay 2021-01-06 15:17:02 +01:00
Yohann D'ANELLO d06a405120
Use a key to use ladders 2021-01-06 14:55:16 +01:00
Eichhornchen 2dc178d67c Merge branch 'equipment' into doc 2021-01-06 14:49:09 +01:00
Eichhornchen 77d501c389 Merge branch 'master' into doc 2021-01-06 14:39:23 +01:00
Yohann D'ANELLO 10788a24ec
Pin Sphinx version 2021-01-06 13:53:42 +01:00
eichhornchen 4ad7d6c37c Added rings that can augment the player's statistics. Also added a new statistic : xp_buff, which helps the player level up sooner. 2021-01-06 11:44:52 +01:00
eichhornchen a9aeb9ca3a Repaired the use functionnality of the main item, as well as the load system for Armor items. 2021-01-06 11:13:17 +01:00
eichhornchen f3fe04e13a The player now has two hands and a slot for a helmet and a chestplate. Accordingly, new classes of items have been added. 2021-01-06 10:46:36 +01:00
eichhornchen 601062237d Repairing part of the tests. 2021-01-05 19:50:25 +01:00
eichhornchen 424044a5e4 Added an overpowered eagle boss. To avoid seing it too often, entities now have a certain chance of being spawned. Closes #19. 2021-01-05 19:40:11 +01:00
eichhornchen 3ace133037 Critical hit chance is now displayed along with the other stats. 2021-01-05 19:23:25 +01:00
eichhornchen 4bddf076ea Fighting now takes into account the constitution. Closes #51 2021-01-05 19:18:25 +01:00
eichhornchen 9b8dfb00da Added critical hit system: the player and rabbit entities have a chance of making x4 damage! Closes #52 2021-01-05 19:08:29 +01:00
eichhornchen 2f95ce3d5f Merge branch 'familiars' into 'master'
Familiars

Closes #54, #46, and #32

See merge request ynerant/squirrel-battle!52
2021-01-05 11:03:39 +01:00
eichhornchen 9cb5c9157f Linting 2021-01-05 10:59:17 +01:00
eichhornchen bb77dab628 Fixed a error induced by the merge: creditsdisplay did not have an update function 2021-01-05 10:49:04 +01:00
eichhornchen 7f63ab2357 Merge branch 'master' into 'familiars'
# Conflicts:
#   squirrelbattle/display/display_manager.py
#   squirrelbattle/display/logsdisplay.py
#   squirrelbattle/display/mapdisplay.py
#   squirrelbattle/display/menudisplay.py
#   squirrelbattle/menus.py
2021-01-05 10:27:39 +01:00
eichhornchen 6341f39fb0 Linting 2021-01-05 10:20:55 +01:00
eichhornchen c378eead74 Fixed the game loading tests : removed trumpets from the test. 2021-01-05 10:11:55 +01:00
eichhornchen f821fef430 added tests 2021-01-05 09:38:49 +01:00
eichhornchen c329150aac Linting again 2020-12-31 17:15:24 +01:00
eichhornchen 73952a42f3 Added tests for familiars 2020-12-31 15:43:38 +01:00
eichhornchen 8e0b8d4fee Added a test for lines 106-107 of game.py 2020-12-31 15:16:40 +01:00
eichhornchen 7cd4721daa linting 2020-12-31 15:00:20 +01:00
eichhornchen de3aba396d Re-changed familiar's inheritance from friendlyEntity to FightingEntity (just a leftover from debug) 2020-12-31 14:51:17 +01:00
Yohann D'ANELLO 663fc0eecd
Better teleport 2020-12-26 21:13:17 +01:00
Yohann D'ANELLO 6b7f8867fa
Tile colors can be overwritten 2020-12-26 14:02:35 +01:00
Yohann D'ANELLO 9a56b4d7e9
Navigate through different maps while climbing ladders 2020-12-26 01:08:43 +01:00
Yohann D'ANELLO 8636d571b5
Add ladders in the map 2020-12-26 00:52:47 +01:00
Yohann D'ANELLO ad5ae22e5f
Manage multiple maps in one game 2020-12-26 00:45:17 +01:00
eichhornchen 505e0a4efb Fixes issue #54, repaired the attribution of the familiars' target 2020-12-21 14:23:58 +01:00
Yohann D'ANELLO 8afa082708 Add Python 3.6 and 3.10 support, fixes #53 2020-12-20 18:02:39 +01:00
Yohann D'ANELLO 0b9606cbfe Update entities documentation 2020-12-20 17:52:44 +01:00
Yohann D'ANELLO 32072ade42 Fix some documentation issues 2020-12-19 13:37:28 +01:00
Yohann D'ANELLO ed6457e94d Test credits menu 2020-12-18 23:39:56 +01:00
Yohann D'ANELLO 4b174f26e4 Better colors 2020-12-18 23:36:08 +01:00
Yohann D'ANELLO 411744bf10 Add credits menu, see #42 2020-12-18 22:24:41 +01:00
eichhornchen ea5f5c1428 Added an original text art to serve as the project's logo. 2020-12-18 21:30:16 +01:00
Nicolas Margulies 762bed888a Working visibility (at least relatively good), but a few lines untested 2020-12-18 21:21:00 +01:00
Charles Peyrat f5e5e365d4 Starting the implementation of the new map generator 2020-12-18 20:02:37 +01:00
Yohann D'ANELLO c01307202a Add shields to be more protected, see #48 2020-12-18 20:01:52 +01:00
Yohann D'ANELLO b876dab156 Register Trumpet as savable entity 2020-12-18 18:13:39 +01:00
Charles Peyrat ba3d979f9c Fix syntax error 2020-12-18 18:10:52 +01:00
Yohann D'ANELLO 9aa684fb77 Use directly equipped items outside the inventory 2020-12-18 17:57:42 +01:00
Yohann D'ANELLO 9475725228 Save equipped item and armor 2020-12-18 17:50:26 +01:00
Yohann D'ANELLO 0394c5d15d Linting 2020-12-18 17:46:38 +01:00
Yohann D'ANELLO e1918ab066 tick function takes the player as argument 2020-12-18 17:40:52 +01:00
Yohann D'ANELLO 1b4612afd0 Swords add strength 2020-12-18 17:39:11 +01:00
Yohann D'ANELLO fe769c4488 We can equip items 2020-12-18 17:30:03 +01:00
eichhornchen dadafc84eb Added a familiar class that follows the player around and hits monsters when it sees one. Added a trumpet, an instance of familiar. Closes #46. 2020-12-18 17:29:59 +01:00
Charles Peyrat 5fbb918132 Add walls even to map borders 2020-12-18 17:05:50 +01:00
Nicolas Margulies 86628fdea6 Working visibility and displaying it, still need to hide things that aren't visible 2020-12-18 17:04:45 +01:00
ynerant 330d78702a Merge branch 'capitalism' into 'master'
Sell items

Closes #50, #49 et #47

See merge request ynerant/squirrel-battle!51
2020-12-18 16:44:55 +01:00
Yohann D'ANELLO 77f52b6276 Screen is refreshed only when pads are all refreshed, fixes #50 2020-12-18 16:40:52 +01:00
Yohann D'ANELLO 5ae49e71ff Display the amount of hazels in the store menu, closes #49 2020-12-18 15:36:25 +01:00
eichhornchen 8ecbf13eae Being able to calculate paths is now a property of fighting entities. 2020-12-18 15:31:23 +01:00
Yohann D'ANELLO 46ce7c33bf Merchant menu is updated through its update function, and does not access globally to the Game instance 2020-12-18 15:15:47 +01:00
Yohann D'ANELLO 85870494a0 More generic display update 2020-12-18 15:07:09 +01:00
Yohann D'ANELLO b8d32b29c8 Test selling items 2020-12-18 02:17:06 +01:00
Yohann D'ANELLO c55a7451e7 Display more precisely where we are in the store menu 2020-12-18 01:50:11 +01:00
Yohann D'ANELLO 9a556ba669 We can now buy items to merchants, closes #47 2020-12-18 01:05:50 +01:00
Yohann D'ANELLO a5890a341d Display inventory menu next to the merchant menu 2020-12-17 23:46:20 +01:00
Nicolas Margulies 62ce2b5c71 Merge remote-tracking branch 'origin/master' into lighting 2020-12-17 21:24:32 +01:00
Yohann D'ANELLO a3e059a97b Some required code mysteriously disappeared 2020-12-15 17:37:23 +01:00
eichhornchen 646e0063be Fixed grammar, unified the docstring's format and added documentation to some classes that did not have any. Closes #32. 2020-12-13 21:29:25 +01:00
ynerant 3f62fbaa2b Merge branch 'erase-pads' into 'master'
Erase pad before resizing

Closes #45

See merge request ynerant/squirrel-battle!50
2020-12-12 21:20:45 +01:00
Yohann D'ANELLO 599f75b676 Erase pad before resizing, fixes #45 2020-12-12 21:19:55 +01:00
ynerant cd5b7086f2 Merge branch 'wait' into 'master'
Add waiting key, fixes #16

Closes #16

See merge request ynerant/squirrel-battle!49
2020-12-12 18:16:31 +01:00
Yohann D'ANELLO 7971a1f70e Add waiting key, fixes #16 2020-12-12 18:12:37 +01:00
ynerant 5eefe1937a Merge branch 'pretty_boxes' into 'master'
Pretty boxes

Closes #44 et #28

See merge request ynerant/squirrel-battle!48
2020-12-12 17:42:38 +01:00
Yohann D'ANELLO 288a6ac2c9 Some translations were missing concerning sunflowers, fixes #44 2020-12-12 17:39:12 +01:00
Yohann D'ANELLO 73e1fac89a Add title to boxes to have pretty boxes, fixes #28 2020-12-12 17:15:08 +01:00
ynerant 158338637a Merge branch 'exploding_bomb' into 'master'
Add a small bomb exploding animation

Closes #31

See merge request ynerant/squirrel-battle!47
2020-12-12 16:53:26 +01:00
Yohann D'ANELLO 8608ce346f Add a small bomb exploding animation, fixes #31 2020-12-12 16:50:01 +01:00
ynerant 1986630da1 Merge branch 'change_image_color' into 'master'
Better color support

Closes #43

See merge request ynerant/squirrel-battle!46
2020-12-12 13:53:04 +01:00
Yohann D'ANELLO 04ae56e451 Higher abstraction level on addmsg, fixes #43 2020-12-12 13:46:45 +01:00
Yohann D'ANELLO 48318a91fe Give more hazels to the player when testing the interaction with merchants 2020-12-11 21:19:25 +01:00
Yohann D'ANELLO 777668848e Clicking on the main image changes its color 2020-12-11 21:17:21 +01:00
Yohann D'ANELLO fb926f8c84 Always use predefined map in game unit tests 2020-12-11 19:27:57 +01:00
Nicolas Margulies 1cf5e7bd8b First implementation of visibility, not tested, nor used for now 2020-12-11 19:23:21 +01:00
Yohann D'ANELLO 209bde5b5c Fix sunflowers and merchants since the position of the player is no longer fixed 2020-12-11 19:21:02 +01:00
Yohann D'ANELLO 895abe88ad Ensure that the neighboorhood is walkable in movement tests 2020-12-11 19:18:48 +01:00
Charles Peyrat 3d7667573e Add testing for the no_lone_walls option 2020-12-11 19:14:28 +01:00
Charles Peyrat fe9dfdf242 Syntax change in randomwalk.Generator.__init__ 2020-12-11 19:13:15 +01:00
Charles Peyrat deb52d7350 Adding a missing parenthesis 2020-12-11 19:05:26 +01:00
Yohann D'ANELLO e91deccb6f Merge branch 'master' into map_generation
# Conflicts:
#	squirrelbattle/interfaces.py
2020-12-11 19:02:45 +01:00
Charles Peyrat 18ca083ba2 Added a connexity test 2020-12-11 18:59:07 +01:00
ynerant 81de0d8eb0 Merge branch 'mouse_interaction' into 'master'
Mouse interaction

Closes #40

See merge request ynerant/squirrel-battle!45
2020-12-11 18:47:20 +01:00
Yohann D'ANELLO a4a10e340d Test clicking on the merchant pad 2020-12-11 18:44:05 +01:00
ynerant 53d090a9c8 Merge branch 'master' into 'mouse_interaction'
# Conflicts:
#   squirrelbattle/display/display_manager.py
#   squirrelbattle/display/menudisplay.py
#   squirrelbattle/entities/items.py
2020-12-11 18:38:13 +01:00
Yohann D'ANELLO f453b82a58 Test clicking on the screen 2020-12-11 18:33:47 +01:00
Charles Peyrat 7667079aa3 Changed Map.large_neighbourhood so we can also request only immediate neighbours, ignoring diagonals 2020-12-11 18:33:16 +01:00
eichhornchen 9d3e8a9822 Merge branch 'village' into 'master'
Village

Closes #41, #38, #37, #36, and #18

See merge request ynerant/squirrel-battle!44
2020-12-11 18:23:10 +01:00
Yohann D'ANELLO d9912cacad Listen to mouse clicks in the main loop 2020-12-11 18:17:59 +01:00
Yohann D'ANELLO 45f420aaef Linting 2020-12-11 18:17:08 +01:00
eichhornchen e7f24c2371 The store menu now displays the price of objects, fixes #41 2020-12-11 18:08:10 +01:00
Yohann D'ANELLO 1afa397fec Better interaction with inventory menu 2020-12-11 18:07:39 +01:00
Charles Peyrat d3c14a48ee Add docstring for Map.large_neighbourhood 2020-12-11 17:46:49 +01:00
Yohann D'ANELLO 089a247b2f Maybe mouse clicks may use the game 2020-12-11 17:43:46 +01:00
Yohann D'ANELLO d50f6701f4 Open a menu with the mouse 2020-12-11 17:40:56 +01:00
Yohann D'ANELLO 99352bc1d5 Test buying an item when we don't have enough of money 2020-12-11 17:28:16 +01:00
Yohann D'ANELLO 7179346e2b Add a InventoryHolder superclass for player and merchants 2020-12-11 17:20:50 +01:00
Charles Peyrat c8b07b3bf5 Only empty tiles should be changed to walls, obviously... 2020-12-11 17:17:11 +01:00
Charles Peyrat 757a460a44 Fix typo 2020-12-11 17:13:12 +01:00
Charles Peyrat 6a4d13c726 Walls now generate around the floor 2020-12-11 17:09:59 +01:00
Charles Peyrat 3a8549cfcc Added a method to interfaces.Map to get the neighbours of a given tile 2020-12-11 17:09:27 +01:00
Yohann D'ANELLO 98b5dd64a8 Linting 2020-12-11 17:06:30 +01:00
Charles Peyrat 7fb743eb72 Switching up start_x and start_y so the player spawn is correctly set 2020-12-11 17:02:22 +01:00
Yohann D'ANELLO bbe37eab97 Listen for clicks, detect which display was clicked 2020-12-11 16:56:22 +01:00
eichhornchen b9b776b7ad Player now pays for what he buys and buying a heart does not put it in the inventory. Solves #38 and #36 2020-12-11 16:49:17 +01:00
eichhornchen 7ba49277a9 Added a symbol for hazels in the ascii texturepack 2020-12-11 15:52:36 +01:00
Yohann D'ANELLO 3c614dcca9 Linting 2020-12-11 02:19:59 +01:00
Yohann D'ANELLO 8751120fe1 Merge master into map_generation, there were some commit behind 2020-12-11 02:17:00 +01:00
Yohann D'ANELLO 7db2dd2b46 Merge branch 'master' into map_generation
# Conflicts:
#	dungeonbattle/game.py
#	squirrelbattle/mapgeneration/__init__.py
#	squirrelbattle/mapgeneration/randomwalk.py
2020-12-11 02:14:47 +01:00
Charles Peyrat 29798c135e Syntax change for the failsafe 2020-12-11 01:24:20 +01:00
Charles Peyrat 45120d0c2b Integrating procedural generation into the game 2020-12-11 01:13:00 +01:00
Charles Peyrat 302017222d Fixing the sampling of the starting position that caused out of bounds error 2020-12-11 01:11:07 +01:00
Charles Peyrat 021731b740 Switching up the tiles used during generation to the correct ones 2020-12-11 01:09:49 +01:00
Charles Peyrat d40a61554e Changing the way the tile matrix is declared so that every column is represented by a different list 2020-12-11 01:04:30 +01:00
nicomarg 53cb6a89ae Merge branch '35-better-pathfinding' into 'master'
Resolve "Better pathfinding"

Closes #35

See merge request ynerant/squirrel-battle!43
2020-12-10 22:31:40 +01:00
Nicolas Margulies 01cc77e146 Fixed a bug when trying to pathfind when player is surrounded by inaccessible tiles 2020-12-10 22:28:12 +01:00
Nicolas Margulies cc6033e8e4 New pathfinding that avoids most of the mobs getting stuck, closes #35 2020-12-10 22:21:09 +01:00
Yohann D'ANELLO 6d4c0b2ca3 Testing the merchant must handle two cases: the bought item is a heart or not 2020-12-09 17:04:29 +01:00
Yohann D'ANELLO 4dbd4f7912 Don't buy hearts, use them instant instead. Fixes #38 2020-12-09 16:57:46 +01:00
Yohann D'ANELLO 2d5a2e4c87 Display hazels in the StatsDisplay 2020-12-09 16:54:53 +01:00
Yohann D'ANELLO 56627f3e84 Reorder texture pack entries 2020-12-09 16:50:47 +01:00
Yohann D'ANELLO 05e79c14e3 Translate the settings entry that change the chat key 2020-12-09 15:45:50 +01:00
Yohann D'ANELLO 405ee895d8 When an item is bought, remove it from the merchant inventory. Fixes #37 2020-12-09 15:35:53 +01:00
Yohann D'ANELLO 19b82ff71a Test merchants 2020-12-09 15:32:37 +01:00
Yohann D'ANELLO 657f4e5209 Remove dead code: friendly entities are fighting entities 2020-12-09 15:10:28 +01:00
Yohann D'ANELLO cea015d015 In load&save test, force to have a merchant 2020-12-09 15:09:07 +01:00
Yohann D'ANELLO 39787861dc Test sunflowers interaction 2020-12-09 15:05:19 +01:00
Nicolas Margulies 50d806cdcf Working Dijkstra 2020-12-08 22:22:20 +01:00
Nicolas Margulies 7823a422b9 Start f new pathfinding, not working 2020-12-08 00:59:19 +01:00
Yohann D'ANELLO 7ab4213273 Fix french translation file 2020-12-07 22:03:48 +01:00
Yohann D'ANELLO e3ae7bcacf Resolve merge conflicts on german translation file 2020-12-07 22:00:27 +01:00
Yohann D'ANELLO ca17bf424a Inventory displays were not updated when the texture pack changed 2020-12-07 21:51:51 +01:00
Yohann D'ANELLO 2bb99a707e Merchant inventory is well-saved 2020-12-07 21:48:56 +01:00
Yohann D'ANELLO 099508d4c0 Don't call screen.getkey() at a wrong place 2020-12-07 21:29:57 +01:00
Yohann D'ANELLO 57fab7db51 Linting 2020-12-07 21:22:06 +01:00
Yohann D'ANELLO b24cc1877f Merchant inventory is working! 2020-12-07 21:13:55 +01:00
eichhornchen 8540a8f354 Work in progress on printing a store menu. Its buggy though 2020-12-07 20:54:53 +01:00
ynerant f648bcd7fb Merge branch 'es-translation' into 'master'
Add spanish translation

See merge request ynerant/squirrel-battle!42
2020-12-07 01:04:55 +01:00
Yohann D'ANELLO fdc2bcab8d Add spanish translation 2020-12-07 01:01:39 +01:00
Charles Peyrat 7cfe55f42c Added a failsafe for cases where the walker population randomly dies out 2020-12-07 00:24:31 +01:00
Charles Peyrat 2a1be4233b Fixed syntax error in Walker.split 2020-12-07 00:18:32 +01:00
Charles Peyrat 32e6eab943 Added import enum.auto to mapgeneration.randomwalk 2020-12-06 23:55:57 +01:00
eichhornchen d39427f978 Merge branch 'master' into village 2020-12-06 11:50:11 +01:00
eichhornchen 866af98fe4 Merging master into village, conflicts were solved 2020-12-06 11:43:48 +01:00
eichhornchen 38842cee68 There was a import error 2020-12-05 21:50:02 +01:00
eichhornchen ba68e68584 Added a Gamemode for selling interfaces, as well as the base of the player/merchant interaction, related to issue #18 2020-12-05 21:43:13 +01:00
ynerant 6fa11d9dfe Merge branch 'body-snatch' into 'master'
Body snatch

Closes #34 et #33

See merge request ynerant/squirrel-battle!38
2020-12-05 14:52:47 +01:00
Yohann D'ANELLO f8ca5b2cc5 Recalculate the path finder when the player exchanges its body 2020-12-05 14:48:43 +01:00
Yohann D'ANELLO 7ad2cad77c Add a message in the logs when a body-snap potion is used 2020-12-05 14:47:54 +01:00
Yohann D'ANELLO f39113fd0b Test body snatch potion, fixes #34 2020-12-05 14:42:13 +01:00
Yohann D'ANELLO c38f8cdc53 Save the inventory of the player when saving the game, fixes #33 2020-12-05 14:35:59 +01:00
Yohann D'ANELLO f887a1f0aa Entity name is a parameter since it can be changed through body snatch potion 2020-12-05 14:25:22 +01:00
Yohann D'ANELLO ea672272f5 Add body snatch potion 2020-12-05 14:20:58 +01:00
ynerant 3985751bd1 Merge branch 'stack-items' into 'master'
Stack items in the inventory

Closes #29

See merge request ynerant/squirrel-battle!37
2020-12-05 13:45:53 +01:00
Yohann D'ANELLO ca2ae15117 Stack items in the inventory, closes #29 2020-12-05 13:42:33 +01:00
ynerant 4d60e95165 Merge branch 'inventory' into 'master'
Inventory

Closes #10

See merge request ynerant/squirrel-battle!36
2020-12-05 13:28:05 +01:00
Yohann D'ANELLO fb8b2aff01 The player must be alive to interact with the inventory 2020-12-05 13:20:52 +01:00
Yohann D'ANELLO bc40f8d0e6 Some translations were missing in the settings menu 2020-12-05 13:19:04 +01:00
Yohann D'ANELLO 013e81b3c0 Better sort in translation files, remove unwanted headers 2020-12-05 13:13:04 +01:00
Yohann D'ANELLO 25c42ea9e8 Don't translate in english since it is the main language 2020-12-05 13:07:37 +01:00
Yohann D'ANELLO 4b9399c687 Test inventory 2020-12-04 18:16:46 +01:00
Charles Peyrat 3717429549 Added some test default parameters for the random walk generator 2020-12-04 18:04:50 +01:00
Charles Peyrat bc9c7cd7f7 Finalised implementation of the walker class with method split 2020-12-04 18:03:41 +01:00
Charles Peyrat a5c53c898e Implemented walker class and methods random_turn, next_pos, move_in_bounds 2020-12-04 18:01:54 +01:00
Yohann D'ANELLO 91a4514649 'D' is not an intelligent key to drop an item 2020-12-04 17:19:06 +01:00
Yohann D'ANELLO 5b4dc601bc Fix broken tests 2020-12-04 17:15:41 +01:00
Yohann D'ANELLO 27fd73c96b Add log messages when a bomb is exploding 2020-12-04 17:10:23 +01:00
Yohann D'ANELLO 056ca5cca8 Ensure that the inventory is not empty before interacting with an item 2020-12-04 17:01:00 +01:00
Yohann D'ANELLO c7545e53f7 Items can be dropped/equipped/used 2020-12-04 16:53:27 +01:00
Yohann D'ANELLO 0da7486750 Navigate through inventory menu 2020-12-04 16:31:15 +01:00
Yohann D'ANELLO a68b3a6d08 Display full inventory 2020-12-04 16:28:37 +01:00
Charles Peyrat 428bbae736 Added base files for map generation and main loop for random walk generation 2020-12-04 16:02:48 +01:00
Yohann D'ANELLO fbfcd5dae0 Inventory title 2020-12-04 16:02:03 +01:00
Yohann D'ANELLO cdd527a7fd Close the inventory using the same key 2020-12-04 14:57:53 +01:00
Yohann D'ANELLO 067570fd1a The inventory is a popup 2020-12-04 14:51:41 +01:00
Yohann D'ANELLO 39af791012 We can open an empty inventory! 2020-12-04 14:45:18 +01:00
eichhornchen 3886bee1ba Friendly entities are now a subclass of fighting entities, and can die. The T key is now used to talk to friendly entities 2020-12-04 00:27:25 +01:00
ynerant 268e2d0dd2 Merge branch 'fix-lag' into 'master'
Fix lag when monsters try to move in a random direction

Closes #25

See merge request ynerant/squirrel-battle!32
2020-12-02 16:07:13 +01:00
Yohann D'ANELLO da0d7e7055 Fix lag when monsters try to move in a random direction 2020-12-02 16:04:43 +01:00
Yohann D'ANELLO 654bab7c1d Translate sun flower messages 2020-12-01 17:12:22 +01:00
Yohann D'ANELLO 29142cd91c Merge remote-tracking branch 'origin/master' into village
# Conflicts:
#	squirrelbattle/display/texturepack.py
#	squirrelbattle/interfaces.py
2020-12-01 17:07:40 +01:00
ynerant e5886bbe44 Merge branch 'translations' into 'master'
Translations

Closes #14

See merge request ynerant/squirrel-battle!31
2020-11-28 16:23:12 +01:00
Yohann D'ANELLO 9f0a29302d Add german translation 2020-11-28 16:22:06 +01:00
Yohann D'ANELLO 6b09d488b6 Documentation on translation 2020-11-28 16:00:17 +01:00
Yohann D'ANELLO aade89de7b Tests and the CI are compiling messages 2020-11-28 14:10:31 +01:00
Yohann D'ANELLO f78c73a703 Tests and the CI are compiling messages 2020-11-28 14:02:23 +01:00
Yohann D'ANELLO a34dae2ad0 Compile messages on setup 2020-11-28 13:49:28 +01:00
Yohann D'ANELLO 5ce62c15f7 Include locale files in Python setup script 2020-11-28 03:23:04 +01:00
Yohann D'ANELLO 7c0cf3e029 CLI to manage messages 2020-11-28 03:21:20 +01:00
Yohann D'ANELLO 8aad15f07b Move translation files in the module 2020-11-28 03:04:28 +01:00
Yohann D'ANELLO ffc8b90441 Create functions that call xgettext or msgfmt 2020-11-28 02:54:04 +01:00
Yohann D'ANELLO 7d02604407 Clean the translation module 2020-11-28 02:00:09 +01:00
Yohann D'ANELLO 138b2c6d54 Logs are capitalized 2020-11-28 01:25:35 +01:00
Yohann D'ANELLO 70ae60b9a4 Translate entities 2020-11-27 22:33:58 +01:00
Yohann D'ANELLO 8f85093eb8 One string was missing 2020-11-27 22:21:52 +01:00
Yohann D'ANELLO d2d74c97a4 Settings menu was not translated 2020-11-27 22:19:41 +01:00
Yohann D'ANELLO f07324662a Cover the language change 2020-11-27 21:56:25 +01:00
Yohann D'ANELLO 31b7ece449 Main menu width must be updated when the language got changed 2020-11-27 21:51:54 +01:00
Yohann D'ANELLO c151e0f656 Menu items are translated 2020-11-27 21:44:17 +01:00
Yohann D'ANELLO 4287b4f045 Add possibility to change the language 2020-11-27 20:53:24 +01:00
Yohann D'ANELLO 2498fd2a61 Translate strings 2020-11-27 20:42:19 +01:00
Yohann D'ANELLO e3be4b4f3f First setup for translation 2020-11-27 20:32:40 +01:00
Yohann D'ANELLO cb18b3881f Fix Debian package version 2020-11-27 19:42:45 +01:00
eichhornchen fbd9f0045b Friendly entities can now talk to the player, a sunflower entity was added to test this new feature. Related to issue #22 2020-11-27 18:38:48 +01:00
eichhornchen d987e60006 Added some documentation for menudisplay 2020-11-27 18:00:54 +01:00
eichhornchen ca86572677 Added a weapon class and a sword subclass 2020-11-27 17:56:01 +01:00
eichhornchen 3f301423fb Added a menudisplay class for menus with a value such as the menu of the merchants 2020-11-27 17:54:55 +01:00
eichhornchen bad56ba442 Added the merchants in the texturepack 2020-11-27 17:54:03 +01:00
eichhornchen 4fdf08cab9 Added a currency : the hazel (for hazelnuts). The player and the merchants possess a certain quantity of hazel 2020-11-27 17:11:59 +01:00
eichhornchen 76bbee7e6d Added a merchant class and a talk_to method to interact with friendly entities 2020-11-27 16:56:22 +01:00
eichhornchen 6b72f4b284 Added a friendly entity class. 2020-11-20 17:42:56 +01:00
eichhornchen 777f155d77 A test 2020-11-20 15:38:01 +01:00
74 changed files with 6699 additions and 733 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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/

View File

@ -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 ÿnérant, eichhornchen, nicomarg, charlse
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 ÿnérant, eichhornchen, nicomarg, charlse
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.

View File

@ -1,6 +1,7 @@
[![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)
[![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/)
@ -8,8 +9,69 @@
# 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.

8
debian/changelog vendored
View File

@ -1,4 +1,10 @@
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.

2
debian/control vendored
View File

@ -2,7 +2,7 @@ Source: python3-squirrel-battle
Section: devel
Priority: optional
Maintainer: ynerant <squirrel-battle@crans.org>
Build-Depends: debhelper (>=10~), dh-python, python3-all, python3-setuptools
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

View File

@ -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,
}

View File

@ -3,7 +3,7 @@ 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_3.14.1_all.deb?job=build-deb
.. _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
@ -34,9 +34,19 @@ paquet ainsi que des détails à fournir à PyPI :
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.1",
version="23.14",
author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knives!",
@ -60,7 +70,7 @@ paquet ainsi que des détails à fournir à PyPI :
],
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",
@ -72,6 +82,8 @@ 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.
@ -160,14 +172,14 @@ du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure :
pkgbase=squirrel-battle
pkgname=python-squirrel-battle-git
pkgver=3.14.1
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=('python-setuptools')
makedepends=('gettext' 'python-setuptools')
depends=('noto-fonts-emoji')
checkdepends=('python-tox')
ssource=("git+https://gitlab.crans.org/ynerant/squirrel-battle.git")
@ -210,17 +222,17 @@ les releases, est plus ou moins similaire :
pkgbase=squirrel-battle
pkgname=python-squirrel-battle
pkgver=3.14.1
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=('python-setuptools')
makedepends=('gettext' 'python-setuptools')
depends=('noto-fonts-emoji')
checkdepends=('python-tox')
source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v3.14.1/$pkgbase-v$pkgver.tar.gz")
source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v23.14/$pkgbase-v$pkgver.tar.gz")
sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0")
build() {
@ -263,7 +275,7 @@ Construction du paquet Debian
-----------------------------
Structure du paquet
-------------------
~~~~~~~~~~~~~~~~~~~
L'ensemble des instructions pour construire le paquet Debian est situé dans le
dossier ``debian/``.
@ -280,14 +292,14 @@ 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.
@ -296,7 +308,7 @@ D'abord on installe les paquets nécessaires :
.. code::
apt update
apt --no-install-recommends install build-essential debmake dh-python debhelper python3-all python3-setuptools
apt --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools
On peut ensuite construire le paquet :
@ -305,7 +317,7 @@ On peut ensuite construire le paquet :
dpkg-buildpackage
mkdir build && cp ../*.deb build/
Le paquet sera installé dans ``build/python3-squirrel-battle_3.14.1_all.deb``.
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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View 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 ``🎺``.

View File

@ -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é.

View File

@ -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 ``🪄``.

View File

@ -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
--------
@ -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 ``🦅``.

View File

@ -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é.

View File

@ -17,6 +17,10 @@ Bienvenue dans la documentation de Squirrel Battle !
:target: https://pypi.org/project/squirrel-battle/
:alt: PyPI
.. 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,7 @@ Bienvenue dans la documentation de Squirrel Battle !
install-dev
tests
display/index
translation
deployment
documentation

View File

@ -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``.

View File

@ -61,7 +61,7 @@ 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.1_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.
@ -73,7 +73,7 @@ Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` :
.. code:: bash
dpkg -i python3-squirrelbattle_3.14.1_all.deb
dpkg -i python3-squirrelbattle_23.14_all.deb
Ce paquet inclut un patch pour afficher les émojis écureuil correctement.

View File

@ -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.

View File

@ -1,2 +1,2 @@
sphinx
sphinx-rtd-theme
sphinx>=3.3
sphinx-rtd-theme>=0.5

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -9,18 +9,30 @@ Pack de textures
.. _Joueur: entities/player.html
.. _Hérisson: entities/monsters.html#herisson
.. _Cœur: entities/items.html#coeur
.. _Bombe: entities/items.html#bombe
.. _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
.. _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_ : ``*``
* Cœur_ : ````
* Bombe_ : ``o``
* Lapin_ : ``Y``
* Tigre_ : ``n``
* Nounours_ : ``8``
* Tournesol_ : ``I``
* Marchand_ : ``M``
* Cœur_ : ````
* Bombe_ : ``o``
* 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_ : ``🦔``
* Cœur_ : ``💜``
* Bombe_ : ``💣``
* Lapin_ : ``🐇``
* Tigre_ : ``🐅``
* Nounours_ : ``🧸``
* Tournesol_ : ``🌻``
* Marchand_ : ``🦜``
* Cœur_ : ``💜``
* Bombe_ : ``💣``
* Explosion_ : ``💥``
* `Potion d'arrachage de corps`_ : ``🔀``
* Épée_ : ``🗡️``
* Bouclier_ : ``🛡️``
* Hazel_ : ``🌰``
* Plastron_ : ``🦺``
* Pygargue_ : ``🦅``
* Casque_ : ``⛑️``
* Anneau_ : ``💍``
* Trompette_ : ``🎺``

120
docs/translation.rst Normal file
View 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)``,
``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.

20
main.py
View File

@ -1,9 +1,25 @@
#!/usr/bin/env python3
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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()

View File

@ -1,18 +1,28 @@
#!/usr/bin/env python3
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os
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.1",
version="23.14",
author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knives!",
@ -36,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",

View File

@ -1,2 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -0,0 +1,44 @@
┃|┃
┃|┃ ▓▓▒ ▓▓
┃|┃ ▓▓ ▓▓▒
┃|┃ ▓▓▓ ▓▓ ▓▓▓ ▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▬█▓▓▓▓▓▓▬█▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓░██░░▓▓░░██░▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
━━▓▓▓▓━━ ▓▓░░░░░░░░ ░░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓ ▓░░░░░░░░░░░░░░░░░░░░▓▓▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓░░░░░░░░▄▄▄▄░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓▓░░░░░░░░░░░░░░░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓ ▓▓▓▓░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓▒▒░░░░░░░░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒
▓▒▒░░░░░░░░░░░░▓▓▓▓▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░░░▓▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒
▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒
▓▓▓▒░░░░░░░░░░░░░▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓ ░
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓ ░░
▓▓▓▓▓▓▓▓▒▒░░░▒▒▒▒░░░░░░▓▓░▒▒▒▓▓▓▓▓▓▓▓▓▓░░░ ░
▓▓▓▓▓▓▓▒░░░░░░░░░▒░░░░░░░░░░░░▒▒▒▓▓▓▓▓▓▓▓░░ ░░▒
░ ░░░░░▒░░░░░░▒░░░▒░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░▒
▒▒░░▓▓░░▒░░░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░░░░░▒░░░░░▒ ░░
▒▒▒▒▒▓▒▒▓░░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░
▒▒█▒█▒▒▒▓░░▒░░░░░░░░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒█▒▒▒▒░░░░▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒░░░
▓█▒▒▒▒█▒█▒▒▒▒░░▒░░░░░▒░░░░▒░░░░░░░░░░░░░░░░░▒░░░░▒░░░░░░░▒░░░░░▒▒
██▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░▒░░░░░░▒░░░░░▒░░░░░
▒▒▒▒█▒▒▒▒▒▒▒░░░░░░░░░░▒░░░░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░
▒▒█▒▒▒▒▒░▒░▒░░░░▓▓▓░░░░░░░▒░░░░▒░░░▒░░░░░░░▓▓░░░░░░░░░░░░ ░
▒▒▒▒▒▒▒░▒░░░▓▓▓▓▓▓░░░░░░░▒░░░░░░░░▒░░░░▓▓▓▓▓▓░░░░░░░░ ░
░▓▓▓▓▓▓░░░░░░▒░░░░░░░░▒░░░░░░▓▓▓▓▓░░░ ░ ░░

View File

@ -1,18 +1,18 @@
1 6
####### #############
#.....# #...........#
#.H...# #...........#
#.....# #####...........#
#.....# #...............#
#.....# #...&........H..#
#.##### #.###...........#
#.# #.# #...........#
#.# #.# #############
#.# #.#
#.#### #.#
#....# #.#
####.###################.#
####&###################&#
#.....................# #################
#.....................# #...............#
#.....................#######...............#
#...........................................#
#.....................&.....&...............#
#.....................#######...............#
####################### #################

View File

@ -1,6 +1,6 @@
1 17
########### #########
#.........# #.......#
#....H....# #.......#
#.........# ############.......#
#.........###############..........#.......##############
#.........#........................#....................#
@ -13,7 +13,7 @@
########.##########......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# ################.######
#...........##..H...# #.........# ################.######
#...........##......# #.........# #.................############
#...........##......# ########.########.......#.........#..........#
#...........##......# #...............#.......#.........#..........#

View File

@ -0,0 +1,41 @@
1 6
################################################################################
#..............................................................................#
#..#...........................................................................#
#...........#..................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
################################################################################

View File

@ -1,9 +1,9 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.game import Game
from squirrelbattle.display.display_manager import DisplayManager
from squirrelbattle.term_manager import TermManager
from .display.display_manager import DisplayManager
from .game import Game
from .term_manager import TermManager
class Bootstrap:

View File

@ -1,2 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,10 +1,12 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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
@ -15,14 +17,24 @@ 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")
@ -30,34 +42,125 @@ class Display:
lines = [line[:width] for line in lines]
return "\n".join(lines)
def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None:
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
"""
Display a message onto the pad.
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:
return pad.addstr(y, x, msg, *options)
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 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()
@ -66,10 +169,10 @@ class Display:
window_y: int, window_x: int,
last_y: int, last_x: int) -> None:
"""
Refresh a pad on a part of the window.
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
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)
@ -81,11 +184,27 @@ class Display:
if last_y >= window_y and last_x >= window_x:
# Refresh the pad only if coordinates are valid
pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x)
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
@ -96,7 +215,9 @@ class Display:
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)
@ -117,7 +238,9 @@ class VerticalSplit(Display):
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)
@ -138,23 +261,58 @@ class HorizontalSplit(Display):
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
pair_number = 4 + self.fg_border_color
self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK)
self.pair = self.color_pair(pair_number)
def display(self) -> None:
self.addstr(self.pad, 0, 0, "" + "" * (self.width - 2) + "",
self.pair)
self.fg_border_color)
for i in range(1, self.height - 1):
self.addstr(self.pad, i, 0, "", self.pair)
self.addstr(self.pad, i, self.width - 1, "", self.pair)
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.pair)
"" + "" * (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)

View File

@ -1,18 +1,17 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit
from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay
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:
@ -23,35 +22,71 @@ 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=screen, pack=None)
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.messagedisplay]
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)
self.messagedisplay.update_message(self.game.message)
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.mapdisplay.pack.tile_width
@ -64,24 +99,70 @@ class DisplayManager:
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)
if self.game.state == GameMode.MAINMENU:
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, x = (self.rows - height) // 2, (self.cols - width) // 2
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,
@ -92,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

View 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)

View File

@ -1,25 +0,0 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
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:
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)

View File

@ -1,50 +0,0 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
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.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.addstr(self.pad, 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
# 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)

View File

@ -1,14 +1,24 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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, Box
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, **kwargs):
@ -17,19 +27,15 @@ class MenuDisplay(Display):
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.addstr(self.pad, i, 0, " " + self.values[i])
def update_pad(self) -> None:
for i in range(self.trueheight):
self.addstr(self.pad, i, 0, " " + self.values[i])
self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line
self.addstr(self.pad, 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 \
@ -40,10 +46,25 @@ class MenuDisplay(Display):
self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase()
self.update_pad()
self.refresh_pad(self.pad, 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
@ -58,32 +79,50 @@ 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.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
- 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)
@ -92,3 +131,244 @@ class MainMenuDisplay(Display):
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

View File

@ -1,31 +0,0 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import Box, Display
class MessageDisplay(Display):
"""
Display a message in a popup.
"""
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_message(self, msg: str) -> None:
self.message = msg
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, curses.A_BOLD)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)

View File

@ -1,47 +0,0 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
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:
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
self.addstr(self.pad, 0, 0, string2)
string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
self.addstr(self.pad, 3, 0, string3)
inventory_str = "Inventaire : " + "".join(
self.pack[item.name.upper()] for item in self.player.inventory)
self.addstr(self.pad, 8, 0, inventory_str)
if self.player.dead:
self.addstr(self.pad, 10, 0, "VOUS ÊTES MORT",
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
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)

View File

@ -1,23 +1,52 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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"
@ -42,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',
TIGER='n',
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='🐇',
TIGER='🐅',
RING_OF_CRITICAL_DAMAGE='💍',
RING_OF_MORE_EXPERIENCE='💍',
RULER='📏',
SHIELD='🛡️ ',
SUNFLOWER='🌻',
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',
WALL='🧱',
SCROLL_OF_DAMAGE='📜',
SCROLL_OF_WEAKENING='📜',
)

View File

@ -1,2 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View 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)

View File

@ -1,74 +1,165 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional
from random import choice, randint
from typing import Any, Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map
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
@ -81,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)

View File

@ -1,7 +1,7 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice
from random import shuffle
from .player import Player
from ..interfaces import FightingEntity, Map
@ -10,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:
@ -21,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)
@ -29,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 \
@ -38,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))
else:
for _ in range(100):
if choice([self.move_up, self.move_down,
self.move_left, self.move_right])():
# 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:
# 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 Tiger(Monster):
"""
A tiger monster
A tiger monster.
"""
def __init__(self, strength: int = 2, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="tiger", 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)

View File

@ -1,33 +1,56 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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:
"""
@ -38,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
@ -81,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
@ -117,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

View File

@ -1,7 +1,7 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from enum import auto, Enum
from typing import Optional
from squirrelbattle.settings import Settings
@ -16,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):
@ -58,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

View File

@ -1,75 +1,116 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from json import JSONDecodeError
from random import randint
from typing import Any, Optional
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.refresh()
while True:
screen.erase()
screen.refresh()
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:
@ -78,35 +119,269 @@ class Game:
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()
@ -131,37 +406,46 @@ 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.
"""
try:
self.map.load_state(d)
except KeyError:
self.message = "Some keys are missing in your save file.\n" \
"Your save seems to be corrupt. It got deleted."
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.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):
@ -170,14 +454,15 @@ class Game:
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."
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()))

View File

@ -1,17 +1,22 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from math import sqrt
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 List, Optional
from typing import Any, Dict, List, Optional, Tuple
from squirrelbattle.display.texturepack import TexturePack
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.
"""
@ -29,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
@ -46,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
@ -75,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()
@ -93,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]
@ -109,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)
@ -118,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
@ -161,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"]
@ -174,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:
@ -201,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,
@ -234,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:
@ -247,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
@ -255,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))
@ -314,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.items import BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
return [Tiger, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
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 Tiger, Hedgehog, Rabbit, \
TeddyBear
from squirrelbattle.entities.items import Bomb, Heart
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 {
"Tiger": Tiger,
"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
@ -357,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
@ -367,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
@ -381,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

View 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"

View 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"

View 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"

View File

View 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

View File

@ -1,17 +1,20 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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
@ -20,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.
@ -95,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)
@ -108,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 []

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path

View File

@ -1,40 +1,44 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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)
@ -50,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:

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
@ -8,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()
@ -20,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()

View File

@ -1,2 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,28 +1,32 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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 Tiger, 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)
@ -37,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 = 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),
"tiger hits tiger. tiger takes 2 damage.")
"Tiger hits tiger. Tiger takes 5 damage.")
self.assertFalse(entity.dead)
self.assertEqual(entity.hit(entity), "tiger hits tiger. "
+ "tiger takes 2 damage. tiger 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)
@ -88,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()
@ -125,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)
@ -149,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)
@ -181,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)

View File

@ -1,37 +1,60 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import curses
import unittest
from squirrelbattle.resources import ResourceManager
from squirrelbattle.enums import DisplayActions
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)
@ -44,6 +67,10 @@ 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:
@ -68,16 +95,15 @@ class TestGame(unittest.TestCase):
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()
@ -108,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(),
@ -172,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)
@ -195,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
@ -223,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()
@ -235,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)
@ -262,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")
@ -275,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)
@ -298,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
@ -314,16 +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:
"""
Display error messages.
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)

View File

@ -1,17 +1,17 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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)
@ -20,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)
@ -28,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())
@ -37,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)

View 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))

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# Copyright (C) 2020-2021 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Tuple
@ -12,8 +12,8 @@ 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 erase(self) -> None:
@ -24,3 +24,6 @@ class FakePad:
def getmaxyx(self) -> Tuple[int, int]:
return 42, 42
def inch(self, y: int, x: int) -> str:
return "i"

View File

@ -1,15 +1,19 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# 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')
@ -24,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')

View 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")

View 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()

View File

@ -19,6 +19,7 @@ deps =
flake8
flake8-annotations
flake8-colors
flake8-import-order
flake8-typing-imports
pep8-naming
pyflakes