Perimeter-based visibility.
[roguelike.git] / creature.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 random
21
22 import cacher
23 import loc
24 import thing
25 import ui
26
27 slotabbrevs = { 'hand': 'W', 'arm': 'A', 'body': 'B', 'head': 'H' }
28
29 class Creature(thing.Thing):
30     def __init__(self, location=None):
31         thing.Thing.__init__(self, location)
32
33         self.dead = False
34         self.hpmax = 10
35         self.hp = self.hpmax
36         self.oxygen = 10
37         
38         self.intrinsics = set()
39         self.extrinsics = set()
40
41         self.inv = loc.Inventory(self)
42         self.slots = { 'hand': None, 'arm': None, 'body': None, 'head': None }
43
44     def has(self, intrinsic):
45         if isinstance(intrinsic, basestring):
46             intrinsic = thing.Intrinsic(intrinsic)
47         return intrinsic in self.intrinsics or intrinsic in self.extrinsics
48
49     def holding(self, name):
50         return self.slots[name]
51     def is_wielding(self, item):
52         return item in self.slots
53     def vis_radius(self):
54         return 5
55     def can_see(self, y, x):
56         loc = self.location
57         if (y - loc.y)**2 + (x - loc.x)**2 <= self.vis_radius()**2:
58             return ((y, x) in self.level().visible_path(loc.y, loc.x, y, x))
59         return False
60
61     def visibles(self):
62         "Return the set of coordinates I can see."
63         vis = set()
64         py = self.location.y
65         px = self.location.x
66         rad = self.vis_radius()
67         
68         
69         # Use Bresenham's circle-drawing algorithm to find the perimeter
70         # of the visibility circle.
71         
72         perimeter = set()
73
74         # Position of the current point relative to (py, px)
75         (ry, rx) = (rad, 0)
76         d = 3 - (2*rad)
77
78         path = self.level().visible_path
79         while rx <= ry:
80             # Map into all 8 octants: SSE, SSW, NNE, NNW, ESE, WSW, ENE, ENW
81             perimeter.add((py + ry, px + rx))
82             perimeter.add((py + ry, px - rx))
83             perimeter.add((py - ry, px + rx))
84             perimeter.add((py - ry, px - rx))
85             perimeter.add((py + rx, px + ry))
86             perimeter.add((py + rx, px - ry))
87             perimeter.add((py - rx, px + ry))
88             perimeter.add((py - rx, px - ry))
89
90             if d < 0:
91                 d += (4*rx) + 6
92             else:
93                 d += 4 * (rx - ry) + 10
94                 ry -= 1
95             rx += 1
96
97         # Now raycast to each tile in the perimeter.  Each ray has max
98         # length ~(rad + 1), and there are ~(2 * PI * rad) coordinates in
99         # the perimeter.  Hence we will make a total of  ~(2 * PI* rad**2)
100         # visits (an average of two per in-radius tile).
101         clip = self.level().clip
102         for (y, x) in perimeter:
103             vis |= set(path(py, px, *clip(y, x)))
104
105         return vis
106
107     def is_creature(self):
108         return True
109
110     def can_breathe(self):
111         if isinstance(self.location, loc.Water):
112             return self.has("waterbreathing")
113         else:
114             return True
115
116     def breathe(self):
117         if self.can_breathe():
118             if self.oxygen < 10:
119                 self.oxygen += 1
120         else:
121             if self.oxygen > 0:
122                 self.oxygen -= 1
123             else:
124                 self.losehp(1, "asphyxia")
125
126     def tick(self):
127         self.breathe()
128
129     def losehp(self, n, cause):
130         if n >= self.hp:
131             self.die(cause)
132         else:
133             self.hp -= n
134
135     def die(self, cause):
136         self.dead = cause
137         loc = self.location
138         self.place(None)
139         for item in self.inv:
140             item.place(loc)
141         thing.ItemClass("corpse")().place(loc)
142
143     def can_swim(self):
144         return random.random() < .5
145     def render(self, pic=None):
146         return ( "?", u"⚗", ui.Color('BrightRed').cp )
147     def move_north(self):
148         return self.try_move(self.location.north())
149     def move_south(self):
150         return self.try_move(self.location.south())
151     def move_west(self):
152         return self.try_move(self.location.west())
153     def move_east(self):
154         return self.try_move(self.location.east())
155     def pick_up(self):
156         it = self.location.top_item()
157         if it:
158             it.try_move(self.inv)
159             return True
160         else:
161             return False
162     def wield(self, item):
163         assert item in self.inv, (
164                 "%s is wielding item %s not in inventory" % (self, item))
165         assert item.wieldable(), (
166                 "%s trying to wield the unwieldable %s" % (self, item))
167         assert not item.wielded, (
168                 "%s trying to wield %s, already wielded by %s"
169                 % (self, item, item.wielded))
170         slot = item.slot
171         if self.holding(slot):
172             self.unwield(self.holding(slot))
173         self.slots[slot] = item
174         item.wielded = (self, slot)
175
176         if item.intrinsics:
177             self.extrinsics |= item.intrinsics
178             
179         return True
180
181     def unwield(self, item):
182         assert item in self.inv, (
183                 "%s is unwielding item %s not in inventory" % (self, item))
184         assert item.wielded, (
185                 "%s trying to unwield already unwielded %s" % (self, item))
186
187         (creat, slot) = item.wielded
188         assert creat is self, "%s unwielding %s's %s" % (self, creat, item)
189         assert self.holding(slot) is item, (
190                 "%s unwielding %s from wrong hand %s?" % (self, item, slot))
191
192         item.wielded = None
193         self.slots[slot] = None
194         if item.intrinsics:
195             self.recompute_extrinsics()
196         return True
197
198     def recompute_extrinsics(self):
199         self.extrinsics.clear()
200         for equip in self.slots.values():
201             if equip and equip.intrinsics:
202                 self.extrinsics |= equip.intrinsics
203
204
205 class Player (Creature):
206     def __init__(self, location=None):
207         super(Player,self).__init__(location)
208         self.active = False
209     def render(self, pic=None):
210         return ("@", u"@", ui.Color(('Bright' if self.active else '') + 'Magenta').cp)
211     def can_swim(self):
212         return random.random() < .5
213     def format_intrinsics(self):
214         return " ".join([ intr.abbrev for intr in self.intrinsics ]
215                 + [ "/" ] + [ extr.abbrev for extr in self.extrinsics ])
216     def invalidate(self):
217         if self.location is not None:
218             self.location.invalidate(important = True)