From a789724e6b1b5eb514f82ac4d7092f7575180c31 Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Sun, 13 Dec 2020 02:23:07 +0100
Subject: [PATCH] Make terrain types configurable.

---
 plomrogue/commands.py | 14 +++++++++++---
 plomrogue/game.py     | 45 +++++++++++++++++++++++++++++++++----------
 plomrogue/mapping.py  | 16 ++++++++-------
 plomrogue/misc.py     | 12 ++++++++++++
 plomrogue/tasks.py    |  5 +++--
 plomrogue/things.py   |  9 ++++++---
 rogue_chat.py         |  3 ++-
 7 files changed, 78 insertions(+), 26 deletions(-)

diff --git a/plomrogue/commands.py b/plomrogue/commands.py
index 026f996..df4b7e2 100644
--- a/plomrogue/commands.py
+++ b/plomrogue/commands.py
@@ -1,5 +1,6 @@
 from plomrogue.misc import quote
 from plomrogue.errors import GameError, ArgError
+from plomrogue.misc import Terrain
 
 
 
@@ -16,11 +17,17 @@ def cmd_THING_TYPES(game, connection_id):
 cmd_THING_TYPES.argtypes = ''
 
 def cmd_TERRAINS(game, connection_id):
-    for t in game.terrains.keys():
-        game.io.send('TERRAIN %s %s' % (quote(t), quote(game.terrains[t])),
-                     connection_id)
+    for t in game.terrains.values():
+        game.io.send('TERRAIN %s %s' % (quote(t.character),
+                                        quote(t.description)), connection_id)
 cmd_TERRAINS.argtypes = ''
 
+def cmd_TERRAIN(game, character, description,
+                blocks_light, blocks_sound, blocks_movement):
+    game.terrains[character] = Terrain(character, description, blocks_light,
+                                       blocks_sound, blocks_movement)
+cmd_TERRAIN.argtypes = 'char string bool bool bool'
+
 def cmd_ALL(game, msg, connection_id):
     speaker = game.get_player(connection_id)
     if not speaker:
@@ -226,6 +233,7 @@ def cmd_MAP(game, geometry, size):
     if geometry == 'Hex':
         map_geometry_class = MapGeometryHex
     game.new_world(map_geometry_class(size))
+    game.terrains = {}
 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos'
 
 def cmd_MAP_CONTROL_LINE(game, big_yx, y, line):
diff --git a/plomrogue/game.py b/plomrogue/game.py
index e1c92cf..7859814 100755
--- a/plomrogue/game.py
+++ b/plomrogue/game.py
@@ -12,7 +12,7 @@ class GameBase:
     def __init__(self):
         self.turn = 0
         self.things = []
-        self.map_geometry = MapGeometrySquare(YX(24, 40))
+        self.map_geometry = MapGeometrySquare(YX(32, 32))
         self.commands = {}
 
     def get_thing(self, id_):
@@ -115,6 +115,7 @@ import os
 class Game(GameBase):
 
     def __init__(self, save_file, *args, **kwargs):
+        from plomrogue.misc import Terrain
         super().__init__(*args, **kwargs)
         self.changed = True
         self.changed_tiles = []
@@ -137,15 +138,11 @@ class Game(GameBase):
         self.last_send_gamestate = datetime.datetime.now() -\
             self.send_gamestate_interval
         self.terrains = {
-            '.': 'floor',
-            'X': 'wall',
-            '=': 'window',
-            '#': 'bed',
-            'T': 'desk',
-            '8': 'cupboard',
-            '[': 'glass door',
-            'o': 'sink',
-            'O': 'toilet'
+            '.': Terrain('.', 'floor'),
+            'X': Terrain('X', 'wall', blocks_light=True, blocks_sound=True,
+                         blocks_movement=True),
+            '=': Terrain('=', 'glass', blocks_sound=True, blocks_movement=True),
+            'T': Terrain('T', 'table', blocks_movement=True),
         }
         if os.path.exists(self.io.save_file):
             if not os.path.isfile(self.io.save_file):
@@ -431,6 +428,28 @@ class Game(GameBase):
             self.player_char_i = 0
         return self.player_chars[self.player_char_i]
 
+    def get_foo_blockers(self, foo):
+        foo_blockers = ''
+        for t in self.terrains.values():
+            block_attr = getattr(t, 'blocks_' + foo)
+            if block_attr:
+                foo_blockers += t.character
+        return foo_blockers
+
+    def get_sound_blockers(self):
+        return self.get_foo_blockers('sound')
+
+    def get_light_blockers(self):
+        return self.get_foo_blockers('light')
+
+    def get_movement_blockers(self):
+        return self.get_foo_blockers('movement')
+
+    def get_flatland(self):
+        for t in self.terrains.values:
+            if not t.blocks_movement:
+                return t.character
+
     def save(self):
 
         def write(f, msg):
@@ -440,6 +459,12 @@ class Game(GameBase):
             write(f, 'TURN %s' % self.turn)
             map_geometry_shape = self.get_map_geometry_shape()
             write(f, 'MAP %s %s' % (map_geometry_shape, self.map_geometry.size,))
+            for terrain in self.terrains.values():
+                write(f, 'TERRAIN %s %s %s %s %s' % (quote(terrain.character),
+                                                     quote(terrain.description),
+                                                     int(terrain.blocks_light),
+                                                     int(terrain.blocks_sound),
+                                                     int(terrain.blocks_movement)))
             for big_yx in [yx for yx in self.maps if self.maps[yx].modified]:
                 for y, line in self.maps[big_yx].lines():
                     write(f, 'MAP_LINE %s %5s %s' % (big_yx, y, quote(line)))
diff --git a/plomrogue/mapping.py b/plomrogue/mapping.py
index 2b19f56..e3c071f 100644
--- a/plomrogue/mapping.py
+++ b/plomrogue/mapping.py
@@ -165,7 +165,7 @@ class Map():
 
     def __init__(self, map_geometry):
         self.geometry = map_geometry
-        self.terrain = '.' * self.size_i
+        self.terrain = '.' * self.size_i  # TODO: use Game.get_flatland()?
 
     def __getitem__(self, yx):
         return self.terrain[self.get_position_index(yx)]
@@ -210,7 +210,9 @@ class Map():
 
 class SourcedMap(Map):
 
-    def __init__(self, things, source_maps, source_center, radius, get_map):
+    def __init__(self, block_chars, things, source_maps, source_center, radius,
+                 get_map):
+        self.block_chars = block_chars
         self.radius = radius
         example_map = get_map(YX(0, 0))
         self.source_geometry = example_map.geometry
@@ -231,7 +233,7 @@ class SourcedMap(Map):
         for yx in self:  # TODO: iter and source_yxyx expensive, cache earlier?
             big_yx, little_yx = self.source_yxyx(yx)
             if big_yx in obstacles and little_yx in obstacles[big_yx]:
-                self.source_map_segment += 'X'
+                self.source_map_segment += self.block_chars[0]
             else:
                 self.source_map_segment += source_maps[big_yx][little_yx]
 
@@ -270,7 +272,7 @@ class DijkstraMap(SourcedMap):
         while shrunk:
             shrunk = False
             for i in range(self.size_i):
-                if self.source_map_segment[i] in 'X=':
+                if self.source_map_segment[i] in self.block_chars:
                     continue
                 neighbors = self.geometry.get_neighbors_i(i)
                 for direction in [d for d in neighbors if neighbors[d]]:
@@ -310,7 +312,8 @@ class FovMap(SourcedMap):
         return self
 
     def throws_shadow(self, yx):
-        return self.source_map_segment[self.get_position_index(yx)] == 'X'
+        return self.source_map_segment[self.get_position_index(yx)]\
+            in self.block_chars
 
     def shadow_process(self, yx, distance_to_center, dir_i, dir_progress):
         # Possible optimization: If no shadow_cones yet and self[yx] == '.',
@@ -377,8 +380,7 @@ class FovMap(SourcedMap):
         return mover(pos)
 
     def circle_out(self, yx, f):
-        # Optimization potential: Precalculate movement positions. (How to check
-        # circle_in_map then?)
+        # Optimization potential: Precalculate movement positions.
         # Optimization potential: Precalculate what tiles are shaded by what tile
         # and skip evaluation of already shaded tile. (This only works if tiles
         # shading implies they completely lie in existing shades; otherwise we
diff --git a/plomrogue/misc.py b/plomrogue/misc.py
index a3f7298..84b0a8a 100644
--- a/plomrogue/misc.py
+++ b/plomrogue/misc.py
@@ -1,3 +1,15 @@
+class Terrain:
+
+    def __init__(self, character, description, blocks_light=False,
+                 blocks_sound=False, blocks_movement=False):
+        self.character = character
+        self.description = description
+        self.blocks_light = blocks_light
+        self.blocks_sound = blocks_sound
+        self.blocks_movement = blocks_movement
+
+
+
 def quote(string):
     """Quote & escape string so client interprets it as single token."""
     quoted = []
diff --git a/plomrogue/tasks.py b/plomrogue/tasks.py
index 4b63634..f7fb312 100644
--- a/plomrogue/tasks.py
+++ b/plomrogue/tasks.py
@@ -34,10 +34,11 @@ class Task_MOVE(Task):
 
     def check(self):
         test_yxyx = self._get_move_target()
+        move_blockers = self.thing.game.get_movement_blockers()
         if test_yxyx in [t.position for t in self.thing.game.things
                          if t.blocking]:
             raise PlayError('blocked by other thing')
-        elif self.thing.game.maps[test_yxyx[0]][test_yxyx[1]] != '.':
+        elif self.thing.game.maps[test_yxyx[0]][test_yxyx[1]] in move_blockers:
             raise PlayError('blocked by impassable tile')
 
     def do(self):
@@ -77,7 +78,7 @@ class Task_FLATTEN_SURROUNDINGS(Task):
                     self.thing.position).values()):
             if not self.thing.game.can_do_tile_with_pw(*yxyx, self.args[0]):
                 continue
-            self.thing.game.maps[yxyx[0]][yxyx[1]] = '.'
+            self.thing.game.maps[yxyx[0]][yxyx[1]] = self.game.get_flatland()
             self.thing.game.record_fov_change(yxyx)
 
 
diff --git a/plomrogue/things.py b/plomrogue/things.py
index 8885aa0..70d9994 100644
--- a/plomrogue/things.py
+++ b/plomrogue/things.py
@@ -70,7 +70,8 @@ class Thing(ThingBase):
         largest_audible_distance = 20
         # player's don't block sound (or should they?)
         things = [t for t in self.game.things if t.type_ != 'Player']
-        dijkstra_map = DijkstraMap(things, self.game.maps, self.position,
+        sound_blockers = self.game.get_sound_blockers()
+        dijkstra_map = DijkstraMap(sound_blockers, things, self.game.maps, self.position,
                                    largest_audible_distance, self.game.get_map)
         url_limits = []
         for m in re.finditer('https?://[^\s]+', msg):
@@ -182,7 +183,8 @@ class Thing_Bottle(Thing):
         # and ThingPlayer.fov_test
         fov_map_class = self.game.map_geometry.fov_map_class
         fov_radius = 12
-        fov = fov_map_class(self.game.things, self.game.maps,
+        light_blockers = self.game.get_light_blockers()
+        fov = fov_map_class(light_blockers, self.game.things, self.game.maps,
                             self.position, fov_radius, self.game.get_map)
         fov.init_terrain()
         visible_players = []
@@ -439,7 +441,8 @@ class ThingAnimate(Thing):
     def prepare_multiprocessible_fov_stencil(self):
         fov_map_class = self.game.map_geometry.fov_map_class
         fov_radius = 3 if self.drunk > 0 else 12
-        self._fov = fov_map_class(self.game.things, self.game.maps,
+        light_blockers = self.game.get_light_blockers()
+        self._fov = fov_map_class(light_blockers, self.game.things, self.game.maps,
                                   self.position, fov_radius, self.game.get_map)
 
     def multiprocessible_fov_stencil(self):
diff --git a/rogue_chat.py b/rogue_chat.py
index 8187583..d7dd64b 100755
--- a/rogue_chat.py
+++ b/rogue_chat.py
@@ -11,7 +11,7 @@ from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_NICK, cmd_PING, cmd_THIN
                                 cmd_GOD_THING_PROTECTION, cmd_THING_PROTECTION,
                                 cmd_SET_MAP_CONTROL_PASSWORD, cmd_SPAWN_POINT,
                                 cmd_THING_MUSICPLAYER_SETTINGS, cmd_THING_HAT_DESIGN,
-                                cmd_THING_MUSICPLAYER_PLAYLIST_ITEM,
+                                cmd_THING_MUSICPLAYER_PLAYLIST_ITEM, cmd_TERRAIN,
                                 cmd_THING_BOTTLE_EMPTY, cmd_PLAYER_FACE,
                                 cmd_GOD_PLAYER_FACE, cmd_GOD_PLAYER_HAT)
 from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE, Task_PICK_UP,
@@ -32,6 +32,7 @@ game.register_command(cmd_LOGIN)
 game.register_command(cmd_NICK)
 game.register_command(cmd_TURN)
 game.register_command(cmd_MAP)
+game.register_command(cmd_TERRAIN)
 game.register_command(cmd_MAP_LINE)
 game.register_command(cmd_MAP_CONTROL_LINE)
 game.register_command(cmd_MAP_CONTROL_PW)
-- 
2.30.2