695d72a34c1bf29030e987c411f17eb907ccdc60
[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("  ."),
181                 random.choice(u" ·."),
182                 random.choice((
183                     ui.Color('BrightBlack').cp, ui.Color('Brown').cp,
184                     ui.Color('BrightGreen').cp, ui.Color('Green').cp,
185                     )))
186         for o in objlist:
187             o.place(self)
188     def passable_by(self, thing):
189         return True
190     def export(self):
191         return (" ", {})
192
193 class Stair(Floor):
194     "A tile that leads to another level."
195     tgt = None
196     oneway = False
197     def __init__(self, lvl, y, x, tgt = None, oneway = False, objlist=[]):
198         Floor.__init__(self, lvl, y, x, objlist)
199         self.pic = ">", u">", ui.Color('White').cp
200         if tgt:
201             self.tgt = tgt
202         if oneway:
203             self.oneway = oneway
204
205     def setattr(self, attr, value):
206         was_available = not (self.tgt or self.oneway)
207         if not attr in ("oneway", "tgt"):
208             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
209
210         self.__setattr__(attr, value)
211
212         is_available = not (self.tgt or self.oneway)
213
214         if was_available and not is_available:
215             self.level().stair_unavailable(self)
216         elif not was_available and is_available:
217             self.level().stair_available(self)
218
219     def target(self):
220         return self.tgt
221
222     def traverse(self):
223         oldlvl = self.level()
224         tgt = self.target()
225
226         if isinstance(tgt, Location):
227             return tgt
228
229         if tgt == None:
230             tgt = level.Level(None, height=40, width=90)
231         elif isinstance(tgt, level.LevelProxy):
232             tgt = tgt.level()
233
234         if isinstance(tgt, level.Level):
235             tgt = tgt.get_target(self.oneway)
236             if not self.oneway:
237                 tgt.setattr("tgt", self)
238         elif isinstance(tgt, level.LocationProxy):
239             tgt = tgt.location()
240             if not self.oneway:
241                 tgt.setattr("tgt", self)
242
243         assert isinstance(tgt, Location), (
244                 "Target of %s is not a Location: %s" % (self, tgt))
245         self.tgt = tgt
246         return tgt
247
248     def export(self):
249         extras = { 'oneway': self.oneway }
250         if (isinstance(self.tgt, Location) or
251                 isinstance(self.tgt, level.LocationProxy)):
252             extras['tgt'] = self.tgt.as_loc()
253         return (">", extras)
254
255 class Door(Tile):
256     "A Tile that may be opened and closed."
257     def __init__(self, lvl, y, x, opened = False, hidden = False):
258         Tile.__init__(self, lvl, y, x)
259         self.opened = opened
260         self.hidden = hidden
261         self.pic = self.getpic()
262     def transparent(self):
263         return self.opened
264     def getpic(self):
265         if self.hidden:
266             if self.opened:
267                 return "=", u"□", ui.Color('Default').cp
268             else:
269                 return "#", u"▒", ui.Color('Default').cp
270         else:
271             if self.opened:
272                 return "-", u"-", ui.Color('Brown').cp
273             else:
274                 return "+", u"+", ui.Color('Brown').cp
275     def open(self):
276         if self.opened:
277             return False
278         else:
279             self.opened = True
280             self.pic = self.getpic()
281             self.invalidate()
282             return True
283     def close(self):
284         if self.opened:
285             self.opened = False
286             self.pic = self.getpic()
287             self.invalidate()
288             return True
289         else:
290             return False
291     def hide(self):
292         if self.hidden:
293             return False
294         else:
295             self.hidden = True
296             self.pic = self.getpic()
297             self.invalidate()
298             return True
299     def unhide(self):
300         if self.hidden:
301             self.hidden = False
302             self.pic = self.getpic()
303             self.invalidate()
304             return True
305         else:
306             return False
307     def setattr(self, attr, value):
308         if attr == 'hidden':
309             self.hide() if value else self.unhide()
310         elif attr == 'opened':
311             self.open() if value else self.hide()
312         else:
313             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
314     def passable_by(self, thing):
315         return self.opened
316     def export(self):
317         return "-" if self.opened else "+", {'hidden': self.hidden, 'opened': self.opened}
318
319 class Wall(Tile):
320     "An impassable Tile."
321     def __init__(self, lvl, y, x):
322         Tile.__init__(self, lvl, y, x)
323         self.pic = "#", u"▒", ui.Color('Default').cp
324     def transparent(self):
325         return False
326     def passable_by(self, thing):
327         return False
328     def export(self):
329         return ("#", {})
330
331 class Water(Tile):
332     "A wet, mostly impassable, Tile."
333     def __init__(self, lvl, y, x, flowy=0.0, flowx=0.0):
334         Tile.__init__(self, lvl, y, x)
335         self.flowy = flowy
336         self.flowx = flowx
337         self.recalc_flow()
338
339     def recalc_flow(self):
340         """Set the appearance of this tile based on the direction of water
341         flow.  Must be called whenever flowy or flowx is changed."""
342         if self.flowy or self.flowx:
343             # Find the sector of the circle at which the flow points:
344             #   ,     ,
345             #  5 \ 6 / 7 
346             # '--_\ /_--'
347             #  4 _=x=_ 0
348             # _-- / \ --_
349             #  3 / 2 \ 1
350             #   '     `
351             angle = math.atan2(self.flowy, self.flowx)
352             idx = int(round(4.0 * angle / math.pi)) % 8
353             self.flowascii = (">\\v/<\\^/")[idx]
354             self.flowuni = (u"→↘↓↙←↖↑↗")[idx]
355         else:
356             self.flowascii = '~'
357             self.flowuni = u'~'
358
359     def render_bg(self):
360         return (self.flowascii, self.flowuni, random.choice((
361             ui.Color('Blue').cp, ui.Color('Blue').cp, ui.Color('Blue').cp,
362             ui.Color('Cyan').cp, ui.Color('BrightBlue').cp, ui.Color('BrightBlue').cp,
363             ui.Color('BrightCyan').cp, ui.Color('White').cp,
364             )))
365
366     def setattr(self, attr, value):
367         if attr in ("flowy", "flowx"):
368             self.__setattr__(attr, value)
369             self.recalc_flow()
370         else:
371             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
372
373     def passable_by(self, thing):
374         return thing.is_item()  or  thing.is_creature() and thing.can_swim()
375     
376     def is_active(self):
377         return True
378    
379     def affect(self, thing):
380         if random.random() < 0.5:
381             (fxf,fxw) = math.modf(self.flowx)
382             (fyf,fyw) = math.modf(self.flowy)
383             xs = 1 if self.flowx > 0.0 else -1
384             ys = 1 if self.flowy > 0.0 else -1
385             
386
387             newx = self.x + int(fxw)
388             newy = self.y + int(fyw)
389             if random.random() < abs(fxf):
390                 newx += xs
391             if random.random() < abs(fyf):
392                 newy += ys
393             (newy,newx) = self.level().clip(newy,newx)
394             thing.try_move(self.level().loc(newy,newx))
395
396     def export(self):
397         return ("~", { 'flowx' : self.flowx, 'flowy' : self.flowy })
398