Start changing conditional expressions to statements.
[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         if len(self._items) > 0:
55             return self._items[0]
56         else:
57             return None
58     def top_creature(self):
59         if len(self._creatures) > 0:
60             return self._creatures[0]
61         else:
62             return None
63     def top_scenery(self):
64         if len(self._scenery) > 0:
65             return self._scenery[0]
66         else:
67             return None
68     def container(self):
69         return self._parent
70     def level(self):
71         return self.container().level()
72     def can_hold(self, thing):
73         return False
74
75 class Inventory (Location):
76     def __init__(self, creature):
77         Location.__init__(self, creature)
78     def __getitem__(self, k):
79         return self._items[k]
80     def can_hold(self, thing):
81         return thing.is_item()
82
83 class Tile (Location):
84     "A square big enough for a player; the smallest unit of space."
85     def __init__(self, lvl, y, x):
86         Location.__init__(self, lvl)
87         (self.y, self.x) = (y, x)
88     def passable_by(self, thing):
89         return None
90     def transparent(self):
91         return True
92     def can_hold(self, thing):
93         return self.passable_by(thing)
94     def affect(self, thing):
95         pass
96     def close(self):
97         return False
98     def open(self):
99         return False
100     def invalidate(self, important=False):
101         self._parent.invalidate(self, important)
102     def add(self, obj):
103         Location.add(self, obj)
104         obj.invalidate()
105     def remove(self, obj):
106         obj.invalidate()
107         Location.remove(self,obj)
108     def target(self):
109         return None
110     def traverse(self):
111         return self.target()
112     def is_active(self):
113         return False
114     def affect_all(self):
115         for i in self._items:
116             self.affect(i)
117         for i in self._creatures:
118             self.affect(i)
119         for i in self._scenery:
120             self.affect(i)
121     def is_adjacent(self, tile):
122         return (self.level() == tile.level()
123                 and abs(self.y - tile.y) <= 1
124                 and abs(self.x - tile.x) <= 1)
125     def setattr(self, attr, value):
126         raise AttributeError("unknown attribute %s for %s" % (attr, self))
127     def open(self):
128         return False
129     def north(self, n=1):
130         newy = self.y - n
131         if newy >= 0:
132             return self.level().loc(newy, self.x)
133         else:
134             return None
135     def south(self, n=1):
136         newy = self.y + n
137         if newy < self.level().h:
138             return self.level().loc(newy, self.x)
139         else:
140             return None
141     def west(self, n=1):
142         newx = self.x - n
143         if newx >= 0:
144             return self.level().loc(self.y, newx)
145         else:
146             return None
147     def east(self, n=1):
148         newx = self.x + n
149         if newx < self.level().w:
150             return self.level().loc(self.y, newx)
151         else:
152             return None
153     def dir(self, key, n=1):
154         if key == curses.KEY_UP:
155             return self.north(n)
156         if key == curses.KEY_DOWN:
157             return self.south(n)
158         if key == curses.KEY_LEFT:
159             return self.west(n)
160         if key == curses.KEY_RIGHT:
161             return self.east(n)
162     def throwdest(self, key, obj, maxdist=6):
163         for i in range(maxdist+1):
164             loc = self.dir(key, i)
165             if loc == None or not loc.passable_by(obj):
166                 if i>0:
167                     return self.dir(key, i-1)
168                 else:
169                     return None
170         return self.dir(key, maxdist)
171     def render_bg(self):
172         return self.pic
173     def render(self):
174         pic = self.render_bg()
175         if self.num_scenery() > 0:
176             pic = self.top_scenery().render(pic)
177         if self.num_items() > 0:
178             pic = self.top_item().render(pic)
179         if self.num_creatures() > 0:
180             pic = self.top_creature().render(pic)
181         return pic
182     def as_loc(self):
183         return level.Loc(self.level().lid, self.y, self.x)
184
185 class Floor(Tile):
186     "A passable Tile."
187     def __init__(self, lvl, y, x, objlist=[]):
188         Tile.__init__(self, lvl, y, x)
189         self.pic = (random.choice("  ."),
190                 random.choice(u" ·."),
191                 random.choice((
192                     ui.Color('BrightBlack').cp, ui.Color('Brown').cp,
193                     ui.Color('BrightGreen').cp, ui.Color('Green').cp,
194                     )))
195         for o in objlist:
196             o.place(self)
197     def passable_by(self, thing):
198         return True
199     def export(self):
200         return (" ", {})
201
202 class Stair(Floor):
203     "A tile that leads to another level."
204     tgt = None
205     oneway = False
206     def __init__(self, lvl, y, x, tgt = None, oneway = False, objlist=[]):
207         Floor.__init__(self, lvl, y, x, objlist)
208         self.pic = ">", u">", ui.Color('White').cp
209         if tgt:
210             self.tgt = tgt
211         if oneway:
212             self.oneway = oneway
213
214     def setattr(self, attr, value):
215         was_available = not (self.tgt or self.oneway)
216         if not attr in ("oneway", "tgt"):
217             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
218
219         self.__setattr__(attr, value)
220
221         is_available = not (self.tgt or self.oneway)
222
223         if was_available and not is_available:
224             self.level().stair_unavailable(self)
225         elif not was_available and is_available:
226             self.level().stair_available(self)
227
228     def target(self):
229         return self.tgt
230
231     def traverse(self):
232         oldlvl = self.level()
233         tgt = self.target()
234
235         if isinstance(tgt, Location):
236             return tgt
237
238         if tgt == None:
239             tgt = level.Level(None, height=40, width=90)
240         elif isinstance(tgt, level.LevelProxy):
241             tgt = tgt.level()
242
243         if isinstance(tgt, level.Level):
244             tgt = tgt.get_target(self.oneway)
245             if not self.oneway:
246                 tgt.setattr("tgt", self)
247         elif isinstance(tgt, level.LocationProxy):
248             tgt = tgt.location()
249             if not self.oneway:
250                 tgt.setattr("tgt", self)
251
252         assert isinstance(tgt, Location), (
253                 "Target of %s is not a Location: %s" % (self, tgt))
254         self.tgt = tgt
255         return tgt
256
257     def export(self):
258         extras = { 'oneway': self.oneway }
259         if (isinstance(self.tgt, Location) or
260                 isinstance(self.tgt, level.LocationProxy)):
261             extras['tgt'] = self.tgt.as_loc()
262         return (">", extras)
263
264 class Door(Tile):
265     "A Tile that may be opened and closed."
266     def __init__(self, lvl, y, x, opened = False, hidden = False):
267         Tile.__init__(self, lvl, y, x)
268         self.opened = opened
269         self.hidden = hidden
270         self.pic = self.getpic()
271     def transparent(self):
272         return self.opened
273     def getpic(self):
274         if self.hidden:
275             if self.opened:
276                 return "=", u"□", ui.Color('Default').cp
277             else:
278                 return "#", u"▒", ui.Color('Default').cp
279         else:
280             if self.opened:
281                 return "-", u"-", ui.Color('Brown').cp
282             else:
283                 return "+", u"+", ui.Color('Brown').cp
284     def open(self):
285         if self.opened:
286             return False
287         else:
288             self.opened = True
289             self.pic = self.getpic()
290             self.invalidate()
291             return True
292     def close(self):
293         if self.opened:
294             self.opened = False
295             self.pic = self.getpic()
296             self.invalidate()
297             return True
298         else:
299             return False
300     def hide(self):
301         if self.hidden:
302             return False
303         else:
304             self.hidden = True
305             self.pic = self.getpic()
306             self.invalidate()
307             return True
308     def unhide(self):
309         if self.hidden:
310             self.hidden = False
311             self.pic = self.getpic()
312             self.invalidate()
313             return True
314         else:
315             return False
316     def setattr(self, attr, value):
317         if attr == 'hidden':
318             if value:
319                 self.hide()
320             else:
321                 self.unhide()
322         elif attr == 'opened':
323             if value:
324                 self.open()
325             else:
326                 self.close()
327         else:
328             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
329     def passable_by(self, thing):
330         return self.opened
331     def export(self):
332         return "-" if self.opened else "+", {'hidden': self.hidden, 'opened': self.opened}
333
334 class Wall(Tile):
335     "An impassable Tile."
336     def __init__(self, lvl, y, x):
337         Tile.__init__(self, lvl, y, x)
338         self.pic = "#", u"▒", ui.Color('Default').cp
339     def transparent(self):
340         return False
341     def passable_by(self, thing):
342         return False
343     def export(self):
344         return ("#", {})
345
346 class Water(Tile):
347     "A wet, mostly impassable, Tile."
348     def __init__(self, lvl, y, x, flowy=0.0, flowx=0.0):
349         Tile.__init__(self, lvl, y, x)
350         self.flowy = flowy
351         self.flowx = flowx
352         self.recalc_flow()
353
354     def recalc_flow(self):
355         """Set the appearance of this tile based on the direction of water
356         flow.  Must be called whenever flowy or flowx is changed."""
357         if self.flowy or self.flowx:
358             # Find the sector of the circle at which the flow points:
359             #   ,     ,
360             #  5 \ 6 / 7 
361             # '--_\ /_--'
362             #  4 _=x=_ 0
363             # _-- / \ --_
364             #  3 / 2 \ 1
365             #   '     `
366             angle = math.atan2(self.flowy, self.flowx)
367             idx = int(round(4.0 * angle / math.pi)) % 8
368             self.flowascii = (">\\v/<\\^/")[idx]
369             self.flowuni = (u"→↘↓↙←↖↑↗")[idx]
370         else:
371             self.flowascii = '~'
372             self.flowuni = u'~'
373
374     def render_bg(self):
375         return (self.flowascii, self.flowuni, random.choice((
376             ui.Color('Blue').cp, ui.Color('Blue').cp, ui.Color('Blue').cp,
377             ui.Color('Cyan').cp, ui.Color('BrightBlue').cp, ui.Color('BrightBlue').cp,
378             ui.Color('BrightCyan').cp, ui.Color('White').cp,
379             )))
380
381     def setattr(self, attr, value):
382         if attr in ("flowy", "flowx"):
383             self.__setattr__(attr, value)
384             self.recalc_flow()
385         else:
386             raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
387
388     def passable_by(self, thing):
389         return thing.is_item()  or  thing.is_creature() and thing.can_swim()
390     
391     def is_active(self):
392         return True
393    
394     def affect(self, thing):
395         if random.random() < 0.5:
396             (fxf,fxw) = math.modf(self.flowx)
397             (fyf,fyw) = math.modf(self.flowy)
398             xs = 1 if self.flowx > 0.0 else -1
399             ys = 1 if self.flowy > 0.0 else -1
400             
401
402             newx = self.x + int(fxw)
403             newy = self.y + int(fyw)
404             if random.random() < abs(fxf):
405                 newx += xs
406             if random.random() < abs(fyf):
407                 newy += ys
408             (newy,newx) = self.level().clip(newy,newx)
409             thing.try_move(self.level().loc(newy,newx))
410
411     def export(self):
412         return ("~", { 'flowx' : self.flowx, 'flowy' : self.flowy })
413