Deavid: Improvements to prevent steam loss
[lightyears.git] / code / map_items.py
blob409c03a7bebb3ae70583751d01f552fed9fb1a4c
2 # 20,000 Light Years Into Space
3 # This game is licensed under GPL v2, and copyright (C) Jack Whitham 2006-07.
6 # Items that you will find on the map.
7 # All inherit from the basic Item.
9 import pygame , math, time, random
10 from pygame.locals import *
12 import bresenham , intersect , extra , stats , resource , draw_obj , sound
13 from primitives import *
14 from steam_model import Steam_Model
15 from mail import New_Mail
19 class Item:
20 def __init__(self, name):
21 self.pos = None
22 self.name_type = name
23 self.draw_obj = None
24 self.emits_steam = False
25 self.tutor_special = False
27 def Draw(self, output):
28 self.draw_obj.Draw(output, self.pos, (0,0))
30 def Draw_Mini(self, output, soffset):
31 self.draw_obj.Draw(output, self.pos, soffset)
33 def Draw_Selected(self, output, highlight):
34 return None
36 def Draw_Popup(self, output):
37 return None
39 def Get_Information(self):
40 return [ ((255,255,0), 20, self.name_type) ]
42 def Prepare_To_Die(self):
43 pass
45 def Take_Damage(self, dmg_level=1):
46 # Basic items have no health and therefore can't be damaged
47 return False
49 def Is_Destroyed(self):
50 return False
52 def Sound_Effect(self):
53 pass
55 class Well(Item):
56 def __init__(self, (x,y), name="Well"):
57 Item.__init__(self, name)
58 self.pos = (x,y)
59 self.draw_obj = draw_obj.Draw_Obj("well.png", 1)
60 self.emits_steam = True
63 class Building(Item):
64 def __init__(self, name):
65 Item.__init__(self, name)
66 self.health = 0
67 self.complete = False
68 self.was_once_complete = False
69 self.max_health = 5 * HEALTH_UNIT
70 self.base_colour = (255,255,255)
71 self.connection_value = 0
72 self.other_item_stack = []
73 self.popup_disappears_at = 0.0
74 self.destroyed = False
75 self.tech_level = 1
78 def Exits(self):
79 return []
81 def Prepare_To_Die(self):
82 self.popup_disappears_at = 0.0
83 self.health = 0
84 self.destroyed = True
86 def Take_Damage(self, dmg_level=1):
87 x = int(dmg_level * DIFFICULTY.DAMAGE_FACTOR)
88 self.health -= x
89 if ( self.health <= 0 ):
90 self.Prepare_To_Die()
91 return True
92 return False
94 def Begin_Upgrade(self):
95 pass
97 def Save(self, other_item):
98 # Used for things that stack on top of other things,
99 # e.g. steam maker on top of well
100 assert isinstance(other_item, Item)
101 assert other_item.pos == self.pos
102 self.other_item_stack.append(other_item)
104 def Restore(self):
105 if ( len(self.other_item_stack) != 0 ):
106 return self.other_item_stack.pop()
107 else:
108 return None
110 def Is_Destroyed(self):
111 return self.destroyed
113 def Needs_Work(self):
114 return ( self.max_health != self.health )
116 def Is_Broken(self):
117 return self.Needs_Work()
119 def Do_Work(self):
120 if ( not self.destroyed ):
121 if ( self.health < self.max_health ):
122 self.health += WORK_UNIT_SIZE
123 # DIFFICULTY.damage_cur_time=time.time();
124 if ( self.health >= self.max_health ):
125 self.health = self.max_health
126 if ( self.was_once_complete ):
127 # An upgrade or repair
128 sound.FX("double")
129 else:
130 # Construction complete!
131 sound.FX("whoosh1")
132 self.complete = True
133 self.was_once_complete = True
137 def Get_Popup_Items(self):
138 return [ self.Get_Health_Meter() ]
140 def Get_Health_Meter(self):
141 return (self.health, (0,255,0), self.max_health, (255,0,0))
143 def Draw_Popup(self, output):
144 (x,y) = Grid_To_Scr(self.pos)
145 x -= 16
146 y -= 12
147 return stats.Draw_Bar_Meter(output, self.Get_Popup_Items(), (x,y), 32, 5)
149 def Get_Tech_Level(self):
150 return ("Tech Level %d" % self.tech_level)
152 def Get_Information(self):
153 l = Item.Get_Information(self)
154 h = (( self.health * 100 ) / self.max_health)
155 h2 = (self.max_health - self.health)
156 units = ""
157 if ( h2 > 0 ):
158 units = str(h2) + " more unit"
159 if ( h2 != 1 ):
160 units += "s"
161 units += " req'd "
163 if ( self.complete ):
164 if ( self.health == self.max_health ):
165 l += [ (self.Get_Diagram_Colour(), 15, "Operational") ]
166 else:
167 l += [ (self.Get_Diagram_Colour(), 15, "Damaged, " + str(h) + "% health"),
168 (None, None, self.Get_Health_Meter()),
169 ((128,128,128), 10, units + "to complete repairs")]
171 l += [ ((128,128,0), 15, self.Get_Tech_Level()) ]
172 else:
173 if ( self.health > 0 ):
174 l += [ (self.Get_Diagram_Colour(), 15, "Building, " + str(h) + "% done"),
175 (None, None, self.Get_Health_Meter()),
176 ((128,128,128), 10, units + "to finish building")]
177 else:
178 l += [ (self.Get_Diagram_Colour(), 15, "Not Built") ]
180 return l
182 def Get_Diagram_Colour(self):
183 (r,g,b) = self.base_colour
184 if ( self.complete ):
185 if ( self.health < self.max_health ):
186 g = ( self.health * g ) / self.max_health
187 b = ( self.health * b ) / self.max_health
188 if ( r < 128 ): r = 128
189 else:
190 if ( self.health > 0 ):
191 r = ( self.health * r ) / self.max_health
192 b = ( self.health * b ) / self.max_health
193 if ( r < 128 ): r = 128
194 else:
195 r = g = b = 128
196 return (r,g,b)
198 class Node(Building):
199 def __init__(self,(x,y),name="Node"):
200 Building.__init__(self,name)
201 self.pipes = []
202 self.pos = (x,y)
203 self.max_health = NODE_HEALTH_UNITS * HEALTH_UNIT
204 self.base_colour = (255,192,0)
205 self.steam = Steam_Model(self)
206 self.draw_obj_finished = draw_obj.Draw_Obj("node.png", 1)
207 self.draw_obj_incomplete = draw_obj.Draw_Obj("node_u.png", 1)
208 self.draw_obj = self.draw_obj_incomplete
210 def Begin_Upgrade(self):
211 if ( self.tech_level >= NODE_MAX_TECH_LEVEL ):
212 New_Mail("Node cannot be upgraded further.")
213 sound.FX("error")
214 elif ( self.Needs_Work() ):
215 New_Mail("Node must be operational before an upgrade can begin.")
216 sound.FX("error")
217 else:
218 sound.FX("crisp")
220 # Upgrade a node to get a higher capacity and more health.
221 # More health means harder to destroy.
222 # More capacity means your network is more resilient.
223 self.tech_level += 1
224 self.max_health += NODE_UPGRADE_WORK * HEALTH_UNIT*self.tech_level
225 self.complete = False
226 self.steam.Capacity_Upgrade()
228 def Steam_Think(self):
230 if (self.name_type!="City" and not self.Needs_Work() and time.time()-DIFFICULTY.damage_cur_time>random.randint(1,self.tech_level*10) and self.tech_level+2<NODE_MAX_TECH_LEVEL):
231 DIFFICULTY.damage_cur_time=time.time();
232 self.Begin_Upgrade();
234 nl = []
235 for p in self.Exits():
236 if ( not p.Is_Broken() ):
237 if ( p.n1 == self ):
238 if ( not p.n2.Is_Broken() ):
239 nl.append((p.n2.steam, p.resistance, p.max_intensity))
240 else:
241 if ( not p.n1.Is_Broken() ):
242 nl.append((p.n1.steam, p.resistance, p.max_intensity))
244 nd = self.steam.Think(nl)
245 for (p, current) in zip(self.Exits(), nd):
246 # current > 0 means outgoing flow
247 if ( current > 0.0 ):
248 p.Flowing_From(self, current)
250 if ( self.Is_Broken() ):
251 self.draw_obj = self.draw_obj_incomplete
252 else:
253 self.draw_obj = self.draw_obj_finished
256 def Exits(self):
257 return self.pipes
259 def Get_Popup_Items(self):
260 return Building.Get_Popup_Items(self) + [
261 self.Get_Pressure_Meter() ]
263 def Get_Pressure_Meter(self):
264 return (int(self.Get_Pressure()), (100, 100, 255),
265 int(self.steam.Get_Capacity()), (0, 0, 100))
267 def Get_Information(self):
268 return Building.Get_Information(self) + [
269 ((128,128,128), 15, "Steam pressure: %1.1f P" % self.steam.Get_Pressure()) ]
271 def Get_Pressure(self):
272 return self.steam.Get_Pressure()
274 def Draw_Selected(self, output, highlight):
275 ra = ( Get_Grid_Size() / 2 ) + 2
276 pygame.draw.circle(output, highlight,
277 Grid_To_Scr(self.pos), ra , 2 )
278 return Grid_To_Scr_Rect(self.pos).inflate(ra,ra)
280 def Sound_Effect(self):
281 sound.FX("bamboo")
284 class City_Node(Node):
285 def __init__(self,(x,y),name="City"):
286 Node.__init__(self,(x,y),name)
287 self.base_colour = CITY_COLOUR
288 self.avail_work_units = 1
289 self.city_upgrade = 0
290 self.city_upgrade_start = 1
291 self.draw_obj = draw_obj.Draw_Obj("city1.png", 3)
292 self.draw_obj_finished = self.draw_obj_incomplete = self.draw_obj
293 self.total_steam = 0
295 def Begin_Upgrade(self):
296 # Upgrade a city for higher capacity
297 # and more work units. Warning: upgraded city
298 # will require more steam!
300 # Most upgrades use the health system as this
301 # puts the unit out of action during the upgrade.
302 # This isn't suitable for cities: you lose if your
303 # city is out of action. We use a special system.
304 if ( self.city_upgrade == 0 ):
305 if ( self.tech_level < DIFFICULTY.CITY_MAX_TECH_LEVEL ):
306 sound.FX("mechanical_1")
308 self.city_upgrade = self.city_upgrade_start = (
309 ( CITY_UPGRADE_WORK + ( self.tech_level *
310 DIFFICULTY.CITY_UPGRADE_WORK_PER_LEVEL )) * HEALTH_UNIT )
311 global WORK_UNIT_SIZE,NODE_MAX_TECH_LEVEL,PIPE_MAX_TECH_LEVEL;
312 self.avail_work_units += 1+self.tech_level/DIFFICULTY.WORK_UNIT_PER_LEVEL # Extra steam demand
313 WORK_UNIT_SIZE = 1+self.tech_level;
314 NODE_MAX_TECH_LEVEL = 1+self.tech_level;
315 PIPE_MAX_TECH_LEVEL = 2+self.tech_level/2;
316 else:
317 New_Mail("City is fully upgraded.")
318 sound.FX("error")
319 else:
320 New_Mail("City is already being upgraded.")
321 sound.FX("error")
323 def Needs_Work(self):
324 return ( self.city_upgrade != 0 )
326 def Is_Broken(self):
327 return False
329 def Do_Work(self):
330 if ( self.city_upgrade > 0 ):
331 self.city_upgrade -= 1
332 if ( self.city_upgrade == 0 ):
333 self.tech_level += 1
334 self.steam.Capacity_Upgrade()
336 sound.FX("cityups")
337 New_Mail("City upgraded to level %d of %d!" %
338 ( self.tech_level, DIFFICULTY.CITY_MAX_TECH_LEVEL ) )
340 def Get_Avail_Work_Units(self):
341 return self.avail_work_units
343 def Get_Steam_Demand(self):
344 return (( self.avail_work_units *
345 WORK_STEAM_DEMAND ) + STATIC_STEAM_DEMAND )
347 def Get_Steam_Supply(self):
348 supply = 0.0
349 for pipe in self.pipes:
350 if ( self == pipe.n1 ):
351 supply -= pipe.current_n1_to_n2
352 else:
353 supply += pipe.current_n1_to_n2
355 return supply
357 def Get_Information(self):
358 l = Node.Get_Information(self)
359 if ( self.city_upgrade != 0 ):
360 l.append( ((255,255,50), 12, "Upgrading...") )
361 l.append( (None, None, self.Get_City_Upgrade_Meter()) )
362 return l
364 def Get_City_Upgrade_Meter(self):
365 if ( self.city_upgrade == 0 ):
366 return (0, (0,0,0), 1, (64,64,64))
367 else:
368 return (self.city_upgrade_start - self.city_upgrade, (255,255,50),
369 self.city_upgrade_start, (64,64,64))
371 def Steam_Think(self):
372 x = self.Get_Steam_Demand()
373 self.total_steam += x
374 self.steam.Source(- x)
375 Node.Steam_Think(self)
377 def Draw(self, output):
378 Node.Draw(self, output)
380 def Get_Popup_Items(self):
381 return [ self.Get_City_Upgrade_Meter() ,
382 self.Get_Pressure_Meter() ]
384 def Take_Damage(self, dmg_level=1): # Can't destroy a city.
385 return False
387 def Draw_Selected(self, output, highlight):
388 r = Grid_To_Scr_Rect(self.pos).inflate(CITY_BOX_SIZE,CITY_BOX_SIZE)
389 pygame.draw.rect(output, highlight,r,2)
390 return r.inflate(2,2)
392 def Get_Tech_Level(self):
393 return Building.Get_Tech_Level(self) + (" of %d" % DIFFICULTY.CITY_MAX_TECH_LEVEL )
395 def Sound_Effect(self):
396 sound.FX("computer")
399 class Well_Node(Node):
400 def __init__(self,(x,y),name="Steam Maker"):
401 Node.__init__(self,(x,y),name)
402 self.base_colour = (255,0,192)
403 self.draw_obj_finished = draw_obj.Draw_Obj("maker.png", 1)
404 self.draw_obj_incomplete = draw_obj.Draw_Obj("maker_u.png", 1)
405 self.draw_obj = self.draw_obj_incomplete
406 self.emits_steam = True
407 self.production = 0
410 def Steam_Think(self):
412 if (self.name_type!="City" and not self.Needs_Work() and time.time()-DIFFICULTY.damage_cur_time>random.randint(1,self.tech_level) and self.tech_level+2<NODE_MAX_TECH_LEVEL):
413 DIFFICULTY.damage_cur_time=time.time();
414 self.Begin_Upgrade();
415 if ( not self.Needs_Work() ):
416 self.production = (DIFFICULTY.BASIC_STEAM_PRODUCTION + (self.tech_level *
417 DIFFICULTY.STEAM_PRODUCTION_PER_LEVEL))
418 max_P=self.steam.capacity
419 min_P=self.steam.charge
421 for pipe in self.pipes:
422 if pipe.n1==self: dest=pipe.n2
423 if pipe.n2==self: dest=pipe.n1
424 if dest.steam.charge<min_P:
425 min_P=dest.steam.charge
427 mult = (max_P - min_P) / max_P
429 self.production*=mult
430 self.steam.Source(self.production)
431 else:
432 self.production = 0
433 Node.Steam_Think(self)
435 def Get_Information(self):
436 return Node.Get_Information(self) + [
437 (self.base_colour, 15,
438 "Steam production: %1.1f U" % self.production) ]
440 def Sound_Effect(self):
441 sound.FX("bamboo1")
444 class Pipe(Building):
445 def __init__(self,n1,n2,name="Pipe"):
446 Building.__init__(self,name)
447 assert n1 != n2
448 n1.pipes.append(self)
449 n2.pipes.append(self)
450 self.n1 = n1
451 self.n2 = n2
452 (x1,y1) = n1.pos
453 (x2,y2) = n2.pos
454 self.pos = ((x1 + x2) / 2, (y1 + y2) / 2)
455 self.length = math.hypot(x1 - x2, y1 - y2)
456 self.max_health = int(self.length + 1) * HEALTH_UNIT
457 self.base_colour = (0,255,0)
458 self.resistance = ( self.length + 2.0 ) * RESISTANCE_FACTOR
459 self.max_intensity = 10
460 self.current_n1_to_n2 = 0.0
462 self.dot_drawing_offset = 0
463 self.dot_positions = []
465 def Begin_Upgrade(self):
466 if ( self.tech_level >= PIPE_MAX_TECH_LEVEL ):
467 New_Mail("Pipe cannot be upgraded further.")
468 sound.FX("error")
469 elif ( self.Needs_Work() ):
470 New_Mail("Pipe must be operational before an upgrade can begin.")
471 sound.FX("error")
472 else:
473 sound.FX("crisp")
474 # Upgrade a pipe for lower resistance and more health.
475 self.tech_level += 1
476 self.max_intensity += 3
477 self.max_health += int( PIPE_UPGRADE_WORK_FACTOR *
478 self.length * HEALTH_UNIT )
479 self.complete = False
480 self.resistance *= PIPE_UPGRADE_RESISTANCE_FACTOR
482 def Exits(self):
483 return [self.n1, self.n2]
485 def Flowing_From(self, node, current):
487 if (self.name_type!="City" and not self.Needs_Work() and time.time()-DIFFICULTY.damage_cur_time>random.randint(1,self.tech_level) and self.tech_level+2<PIPE_MAX_TECH_LEVEL):
488 DIFFICULTY.damage_cur_time=time.time();
489 self.Begin_Upgrade();
491 if ( node == self.n1 ):
492 self.current_n1_to_n2 = current
493 elif ( node == self.n2 ):
494 self.current_n1_to_n2 = - current
495 else:
496 assert False
498 def Take_Damage(self, dmg_level=1):
499 # Pipes have health proportional to their length.
500 # To avoid a rules loophole, damage inflicted on
501 # pipes is multiplied by their length. Pipes are
502 # a very soft target.
503 return Building.Take_Damage(self, dmg_level * (self.length + 1.0))
505 def Draw_Mini(self, output, (x,y) ):
506 (x1,y1) = Grid_To_Scr(self.n1.pos)
507 (x2,y2) = Grid_To_Scr(self.n2.pos)
508 x1 -= x ; x2 -= x
509 y1 -= y ; y2 -= y
511 if ( self.Needs_Work() ):
512 c = (255,0,0)
513 else:
514 c = self.Get_Diagram_Colour()
516 pygame.draw.line(output, c, (x1,y1), (x2,y2), 2)
518 if ( not self.Needs_Work() ):
519 mx = ( x1 + x2 ) / 2
520 my = ( y1 + y2 ) / 2
521 if ( output.get_rect().collidepoint((mx,my)) ):
522 info_text = "%1.1f U" % abs(self.current_n1_to_n2)
523 info_surf = stats.Get_Font(12).render(info_text, True, c)
524 r2 = info_surf.get_rect()
525 r2.center = (mx,my)
526 r = Rect(r2)
527 r.width += 4
528 r.center = (mx,my)
529 pygame.draw.rect(output, (0, 40, 0), r)
530 output.blit(info_surf, r2.topleft)
533 def Draw(self,output):
534 (x1,y1) = Grid_To_Scr(self.n1.pos)
535 (x2,y2) = Grid_To_Scr(self.n2.pos)
536 if ( self.Needs_Work() ):
537 # Plain red line
538 pygame.draw.line(output, (255,0,0), (x1,y1), (x2,y2), 3)
539 self.dot_drawing_offset = 0
540 return
543 # Dark green backing line:
544 colour = (32,128,20)
545 pygame.draw.line(output, colour, (x1,y1), (x2,y2), 3)
547 if ( self.current_n1_to_n2 == 0.0 ):
548 return
550 r = Rect(0,0,1,1)
551 for pos in self.dot_positions:
552 r.center = pos
553 output.fill(colour, r)
555 # Thanks to Acidd_UK for the following suggestion.
556 dots = int(( self.length * 0.3 ) + 1.0)
557 positions = dots * self.SFACTOR
559 pos_a = (x1, y1 + 1)
560 pos_b = (x2, y2 + 1)
561 interp = self.dot_drawing_offset
562 colour = (0, 255, 0) # brigt green dots
564 self.dot_positions = [
565 extra.Partial_Vector(pos_a, pos_b, (interp, positions))
566 for interp in range(self.dot_drawing_offset, positions,
567 self.SFACTOR) ]
569 for pos in self.dot_positions:
570 r.center = pos
571 output.fill(colour, r)
573 # Tune these to alter the speed of the dots.
574 SFACTOR = 512
575 FUTZFACTOR = 4.0 * 35.0
577 def Frame_Advance(self, frame_time):
578 self.dot_drawing_offset += int(self.FUTZFACTOR *
579 frame_time * self.current_n1_to_n2)
581 if ( self.dot_drawing_offset < 0 ):
582 self.dot_drawing_offset = (
583 self.SFACTOR - (( - self.dot_drawing_offset ) % self.SFACTOR ))
584 else:
585 self.dot_drawing_offset = self.dot_drawing_offset % self.SFACTOR
587 def Make_Ready_For_Save(self):
588 self.dot_positions = []
590 def __Draw_Original(self, output):
591 (x1,y1) = Grid_To_Scr(self.n1.pos)
592 (x2,y2) = Grid_To_Scr(self.n2.pos)
593 if ( self.Needs_Work() ):
594 c = (255,0,0)
595 else:
596 c = self.Get_Diagram_Colour()
597 pygame.draw.line(output, c, (x1,y1), (x2,y2), 2)
599 def Draw_Selected(self, output, highlight):
600 p1 = Grid_To_Scr(self.n1.pos)
601 p2 = Grid_To_Scr(self.n2.pos)
602 pygame.draw.line(output, highlight, p1, p2, 5)
603 #self.Draw(output) # Already done elsewhere.
605 return Rect(p1,(1,1)).union(Rect(p2,(1,1))).inflate(7,7)
607 def Get_Information(self):
608 return Building.Get_Information(self) + [
609 ((128,128,128), 15, "%1.1f km" % self.length) ,
610 ((128,128,128), 15, "Flow rate: %1.1f U" % abs(self.current_n1_to_n2) ) ]
612 def Sound_Effect(self):
613 sound.FX("bamboo2")