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
20 def __init__(self
, name
):
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
):
36 def Draw_Popup(self
, output
):
39 def Get_Information(self
):
40 return [ ((255,255,0), 20, self
.name_type
) ]
42 def Prepare_To_Die(self
):
45 def Take_Damage(self
, dmg_level
=1):
46 # Basic items have no health and therefore can't be damaged
49 def Is_Destroyed(self
):
52 def Sound_Effect(self
):
56 def __init__(self
, (x
,y
), name
="Well"):
57 Item
.__init
__(self
, name
)
59 self
.draw_obj
= draw_obj
.Draw_Obj("well.png", 1)
60 self
.emits_steam
= True
64 def __init__(self
, name
):
65 Item
.__init
__(self
, name
)
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
81 def Prepare_To_Die(self
):
82 self
.popup_disappears_at
= 0.0
86 def Take_Damage(self
, dmg_level
=1):
87 x
= int(dmg_level
* DIFFICULTY
.DAMAGE_FACTOR
)
89 if ( self
.health
<= 0 ):
94 def Begin_Upgrade(self
):
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
)
105 if ( len(self
.other_item_stack
) != 0 ):
106 return self
.other_item_stack
.pop()
110 def Is_Destroyed(self
):
111 return self
.destroyed
113 def Needs_Work(self
):
114 return ( self
.max_health
!= self
.health
)
117 return self
.Needs_Work()
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
130 # Construction complete!
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
)
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
)
158 units
= str(h2
) + " more unit"
163 if ( self
.complete
):
164 if ( self
.health
== self
.max_health
):
165 l
+= [ (self
.Get_Diagram_Colour(), 15, "Operational") ]
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()) ]
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")]
178 l
+= [ (self
.Get_Diagram_Colour(), 15, "Not Built") ]
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
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
198 class Node(Building
):
199 def __init__(self
,(x
,y
),name
="Node"):
200 Building
.__init
__(self
,name
)
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.")
214 elif ( self
.Needs_Work() ):
215 New_Mail("Node must be operational before an upgrade can begin.")
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.
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();
235 for p
in self
.Exits():
236 if ( not p
.Is_Broken() ):
238 if ( not p
.n2
.Is_Broken() ):
239 nl
.append((p
.n2
.steam
, p
.resistance
, p
.max_intensity
))
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
253 self
.draw_obj
= self
.draw_obj_finished
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
):
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
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;
317 New_Mail("City is fully upgraded.")
320 New_Mail("City is already being upgraded.")
323 def Needs_Work(self
):
324 return ( self
.city_upgrade
!= 0 )
330 if ( self
.city_upgrade
> 0 ):
331 self
.city_upgrade
-= 1
332 if ( self
.city_upgrade
== 0 ):
334 self
.steam
.Capacity_Upgrade()
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
):
349 for pipe
in self
.pipes
:
350 if ( self
== pipe
.n1
):
351 supply
-= pipe
.current_n1_to_n2
353 supply
+= pipe
.current_n1_to_n2
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()) )
364 def Get_City_Upgrade_Meter(self
):
365 if ( self
.city_upgrade
== 0 ):
366 return (0, (0,0,0), 1, (64,64,64))
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.
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
):
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
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
)
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
):
444 class Pipe(Building
):
445 def __init__(self
,n1
,n2
,name
="Pipe"):
446 Building
.__init
__(self
,name
)
448 n1
.pipes
.append(self
)
449 n2
.pipes
.append(self
)
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.")
469 elif ( self
.Needs_Work() ):
470 New_Mail("Pipe must be operational before an upgrade can begin.")
474 # Upgrade a pipe for lower resistance and more health.
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
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
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
)
511 if ( self
.Needs_Work() ):
514 c
= self
.Get_Diagram_Colour()
516 pygame
.draw
.line(output
, c
, (x1
,y1
), (x2
,y2
), 2)
518 if ( not self
.Needs_Work() ):
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()
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() ):
538 pygame
.draw
.line(output
, (255,0,0), (x1
,y1
), (x2
,y2
), 3)
539 self
.dot_drawing_offset
= 0
543 # Dark green backing line:
545 pygame
.draw
.line(output
, colour
, (x1
,y1
), (x2
,y2
), 3)
547 if ( self
.current_n1_to_n2
== 0.0 ):
551 for pos
in self
.dot_positions
:
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
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
,
569 for pos
in self
.dot_positions
:
571 output
.fill(colour
, r
)
573 # Tune these to alter the speed of the dots.
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
))
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() ):
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
):