00c980924772173191995d1db5d039aecb187ba8
[roguelike.git] / loc.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # Copyright © 2008 Neil Moore <neil@s-z.org>.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #  
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 import curses
21 import random
22 import math
23
24 import level
25 import ui
26
27 class Location (object):
28     def __init__(self, container = None):
29         self._items = []      # in order from top to bottom
30         self._creatures = []
31         self._scenery = []
32         self._parent = container
33     def num_items(self):
34         return len(self._items)
35     def num_creatures(self):
36         return len(self._creatures)
37     def num_scenery(self):
38         return len(self._scenery)
39     def remove(self, obj):
40         if obj.is_item():
41             self._items.remove(obj)
42         elif obj.is_creature():
43             self._creatures.remove(obj)
44         elif obj.is_scenery():
45             self._scenery.remove(obj)
46     def add(self, obj):
47         if obj.is_item():
48             self._items[:0] = [obj]
49         elif obj.is_creature():
50             self._creatures[:0] = [obj]
51         elif obj.is_scenery():
52             self._scenery[:0] = [obj]
53     def top_item(self):
54         return self._items[0] if len(self._items) > 0 else None
55     def top_creature(self):
56         return self._creatures[0] if len(self._creatures) > 0 else None
57     def top_scenery(self):
58         return self._scenery[0] if len(self._scenery) > 0 else None
59     def container(self):
60         return self._parent
61     def level(self):
62         return self.container().level()
63     def can_hold(self, thing):
64         return False
65
66 class Inventory (Location):
67     def __init__(self, creature):
68         Location.__init__(self, creature)
69     def __getitem__(self, k):
70         return self._items[k]
71     def can_hold(self, thing):
72         return thing.is_item()
73
74 class Tile (Location):
75     "A square big enough for a player; the smallest unit of space."
76     def __init__(self, lvl, y, x):
77         Location.__init__(self, lvl)
78         (self.y, self.x) = (y, x)
79     def passable_by(self, thing):
80         return None
81     def transparent(self):
82         return True
83     def can_hold(self, thing):
84         return self.passable_by(thing)
85     def affect(self, thing):
86         pass
87     def close(self):
88         return False
89     def open(self):
90         return False
91     def invalidate(self, important=False):
92         self._parent.invalidate(self, important)
93     def add(self, obj):
94         Location.add(self, obj)
95         obj.invalidate()
96     def remove(self, obj):
97         obj.invalidate()
98         Location.remove(self,obj)
99     def target(self):
100         return None
101     def traverse(self):
102         return self.target()
103     def is_active(self):
104         return False
105     def affect_all(self):
106         for i in self._items:
107             self.affect(i)
108         for i in self._creatures:
109             self.affect(i)
110         for i in self._scenery:
111             self.affect(i)
112     def is_adjacent(self, tile):
113         return (self.level() == tile.level()
114                 and abs(self.y - tile.y) <= 1
115                 and abs(self.x - tile.x) <= 1)
116     def setattr(self, attr, value):
117         raise AttributeError("unknown attribute %s for %s" % (attr, self))
118     def open(self):
119         return False
120     def north(self, n=1):
121         newy = self.y - n
122         if newy >= 0:
123             return self.level().loc(newy, self.x)
124         else:
125             return None
126     def south(self, n=1):
127         newy = self.y + n
128         if newy < self.level().h:
129             return self.level().loc(newy, self.x)
130         else:
131             return None
132     def west(self, n=1):
133         newx = self.x - n
134         if newx >= 0:
135             return self.level().loc(self.y, newx)
136         else:
137             return None
138     def east(self, n=1):
139         newx = self.x + n
140         if newx < self.level().w:
141             return self.level().loc(self.y, newx)
142         else:
143             return None
144     def dir(self, key, n=1):
145         if key == curses.KEY_UP:
146             return self.north(n)
147         if key == curses.KEY_DOWN:
148             return self.south(n)
149         if key == curses.KEY_LEFT:
150             return self.west(n)
151         if key == curses.KEY_RIGHT:
152             return self.east(n)
153     def throwdest(self, key, obj, maxdist=6):
154         for i in range(maxdist+1):
155             loc = self.dir(key, i)
156             if loc == None or not loc.passable_by(obj):
157                 if i>0:
158                     return self.dir(key, i-1)
159                 else:
160                     return None
161         return self.dir(key, maxdist)
162     def render_bg(self):
163         return self.pic
164     def render(self):
165         pic = self.render_bg()
166         if self.num_scenery() > 0:
167             pic = self.top_scenery().render(pic)
168         if self.num_items() > 0:
169             pic = self.top_item().render(pic)
170         if self.num_creatures() > 0:
171             pic = self.top_creature().render(pic)
172         return pic
173     def as_loc(self):
174         return level.Loc(self.level().lid, self.y, self.x)
175
176 class Floor(Tile):
177     "A passable Tile."
178     def __init__(self, lvl, y, x, objlist=[]):
179         Tile.__init__(self, lvl, y, x)
180         self.pic = random.choice("  ."), random.choice((
181             ui.Color('BrightBlack').cp, ui.Color('Brown').cp,
182             ui.Color('BrightGreen').cp, ui.Color('Green').cp,
183             ))
184         for o in objlist:
185             o.place(self)
186     def passable_by(self, thing):
187         return True
188     def export(self):
189         return (" ", {})
190
191 class Stair(Floor):
192     "A tile that leads to another level."
193     tgt = None
194     oneway = False
195     def __init__(self, lvl, y, x, tgt = None, oneway = False, objlist=[]):
196         Floor.__init__(self, lvl, y, x, objlist)
197         self.pic = ">", ui.Color('White').cp
198         if tgt:
199             self.tgt = tgt
200         if oneway:
201             self.oneway = oneway
202
203     def setattr(self, attr, value):
204         was_available = not (self.tgt or self.oneway)
205         if not attr in ("oneway", "tgt"):
206             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
207
208         self.__setattr__(attr, value)
209
210         is_available = not (self.tgt or self.oneway)
211
212         if was_available and not is_available:
213             self.level().stair_unavailable(self)
214         elif not was_available and is_available:
215             self.level().stair_available(self)
216
217     def target(self):
218         return self.tgt
219
220     def traverse(self):
221         oldlvl = self.level()
222         tgt = self.target()
223
224         if isinstance(tgt, Location):
225             return tgt
226
227         if tgt == None:
228             tgt = level.Level(None, height=40, width=90)
229         elif isinstance(tgt, level.LevelProxy):
230             tgt = tgt.level()
231
232         if isinstance(tgt, level.Level):
233             tgt = tgt.get_target(self.oneway)
234             if not self.oneway:
235                 tgt.setattr("tgt", self)
236         elif isinstance(tgt, level.LocationProxy):
237             tgt = tgt.location()
238             if not self.oneway:
239                 tgt.setattr("tgt", self)
240
241         assert isinstance(tgt, Location), (
242                 "Target of %s is not a Location: %s" % (self, tgt))
243         self.tgt = tgt
244         return tgt
245
246     def export(self):
247         extras = { 'oneway': self.oneway }
248         if (isinstance(self.tgt, Location) or
249                 isinstance(self.tgt, level.LocationProxy)):
250             extras['tgt'] = self.tgt.as_loc()
251         return (">", extras)
252
253 class Door(Tile):
254     "A Tile that may be opened and closed."
255     def __init__(self, lvl, y, x, opened = False, hidden = False):
256         Tile.__init__(self, lvl, y, x)
257         self.opened = opened
258         self.hidden = hidden
259         self.pic = self.getpic()
260     def transparent(self):
261         return self.opened
262     def getpic(self):
263         if self.hidden:
264             return "=" if self.opened else "#", ui.Color('Default').cp
265         else:
266             return "-" if self.opened else "+", ui.Color('Brown').cp
267     def open(self):
268         if self.opened:
269             return False
270         else:
271             self.opened = True
272             self.pic = self.getpic()
273             self.invalidate()
274             return True
275     def close(self):
276         if self.opened:
277             self.opened = False
278             self.pic = self.getpic()
279             self.invalidate()
280             return True
281         else:
282             return False
283     def hide(self):
284         if self.hidden:
285             return False
286         else:
287             self.hidden = True
288             self.pic = self.getpic()
289             self.invalidate()
290             return True
291     def unhide(self):
292         if self.hidden:
293             self.hidden = False
294             self.pic = self.getpic()
295             self.invalidate()
296             return True
297         else:
298             return False
299     def setattr(self, attr, value):
300         if attr == 'hidden':
301             self.hide() if value else self.unhide()
302         elif attr == 'opened':
303             self.open() if value else self.hide()
304         else:
305             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
306     def passable_by(self, thing):
307         return self.opened
308     def export(self):
309         return "-" if self.opened else "+", {'hidden': self.hidden, 'opened': self.opened}
310
311 class Wall(Tile):
312     "An impassable Tile."
313     def __init__(self, lvl, y, x):
314         Tile.__init__(self, lvl, y, x)
315         self.pic = "#", ui.Color('Default').cp
316     def transparent(self):
317         return False
318     def passable_by(self, thing):
319         return False
320     def export(self):
321         return ("#", {})
322
323 class Water(Tile):
324     "A wet, mostly impassable, Tile."
325     def __init__(self, lvl, y, x, flowy=0.0, flowx=0.0):
326         Tile.__init__(self, lvl, y, x)
327         self.flowy = flowy
328         self.flowx = flowx
329         self.recalc_flow()
330
331     def recalc_flow(self):
332         """Set the appearance of this tile based on the direction of water
333         flow.  Must be called whenever flowy or flowx is changed."""
334         if self.flowy or self.flowx:
335             # Find the sector of the circle at which the flow points:
336             #   ,     ,
337             #  5 \ 6 / 7 
338             # '--_\ /_--'
339             #  4 _=x=_ 0
340             # _-- / \ --_
341             #  3 / 2 \ 1
342             #   '     `
343             angle = math.atan2(self.flowy, self.flowx)
344             idx = int(round(4.0 * angle / math.pi)) % 8
345             self.flowsign = (">\\v/<\\^/")[idx]
346         else:
347             self.flowsign = '~'
348
349     def render_bg(self):
350         return (self.flowsign, random.choice((
351             ui.Color('Blue').cp, ui.Color('Blue').cp, ui.Color('Blue').cp,
352             ui.Color('Cyan').cp, ui.Color('BrightBlue').cp, ui.Color('BrightBlue').cp,
353             ui.Color('BrightCyan').cp, ui.Color('White').cp,
354             )))
355
356     def setattr(self, attr, value):
357         if attr in ("flowy", "flowx"):
358             self.__setattr__(attr, value)
359             self.recalc_flow()
360         else:
361             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
362
363     def passable_by(self, thing):
364         return thing.is_item()  or  thing.is_creature() and thing.can_swim()
365     
366     def is_active(self):
367         return True
368    
369     def affect(self, thing):
370         if random.random() < 0.5:
371             (fxf,fxw) = math.modf(self.flowx)
372             (fyf,fyw) = math.modf(self.flowy)
373             xs = 1 if self.flowx > 0.0 else -1
374             ys = 1 if self.flowy > 0.0 else -1
375             
376
377             newx = self.x + int(fxw)
378             newy = self.y + int(fyw)
379             if random.random() < abs(fxf):
380                 newx += xs
381             if random.random() < abs(fyf):
382                 newy += ys
383             (newy,newx) = self.level().clip(newy,newx)
384             thing.try_move(self.level().loc(newy,newx))
385
386     def export(self):
387         return ("~", { 'flowx' : self.flowx, 'flowy' : self.flowy })
388