/
rogue.py
2525 lines (2212 loc) · 90.2 KB
/
rogue.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
rogue.py
Softly Into the Night, a sci-fi/Lovecraftian roguelike
Copyright (C) 2019 Jacob Wharton.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
This file glues everything together.
'''
import esper
import tcod as libtcod
import math
from const import *
from colors import COLORS as COL
import components as cmp
import processors as proc
import orangio as IO
import action
import debug
import dice
import entities
import game
import levelgen
import lights
import misc
import managers
import maths
import player
import tilemap
EQUIPABLE_CONSTS={
EQ_MAINHAND : cmp.EquipableInHoldSlot,
EQ_OFFHAND : cmp.EquipableInHoldSlot,
EQ_MAINARM : cmp.EquipableInArmSlot,
EQ_OFFARM : cmp.EquipableInArmSlot,
EQ_MAINLEG : cmp.EquipableInLegSlot,
EQ_OFFLEG : cmp.EquipableInLegSlot,
EQ_MAINFOOT : cmp.EquipableInFootSlot,
EQ_OFFFOOT : cmp.EquipableInFootSlot,
EQ_FRONT : cmp.EquipableInFrontSlot,
EQ_BACK : cmp.EquipableInBackSlot,
EQ_CORE : cmp.EquipableInCoreSlot,
EQ_HIPS : cmp.EquipableInHipsSlot,
EQ_MAINHEAD : cmp.EquipableInHeadSlot,
EQ_MAINFACE : cmp.EquipableInFaceSlot,
EQ_MAINNECK : cmp.EquipableInNeckSlot,
EQ_MAINEYES : cmp.EquipableInEyesSlot,
EQ_MAINEARS : cmp.EquipableInEarsSlot,
EQ_AMMO : cmp.EquipableInAmmoSlot,
}
#----------------#
# global objects #
#----------------#
class Rogue:
occupations={}
et_managers={} #end of turn managers
bt_managers={} #beginning of turn managers
c_managers={} #const managers
manager = None # current active game state manager
@classmethod
def run_endTurn_managers(cls, pc):
for v in cls.et_managers.values():
v.run(pc)
@classmethod
def run_beginTurn_managers(cls, pc):
for v in cls.bt_managers.values():
v.run(pc)
@classmethod
def create_settings(cls): # later controllers might depend on settings
cls.settings = game.GlobalSettings()
cls.settings.read() # go ahead and read/apply settings
cls.settings.apply()
@classmethod
def create_world(cls): cls.world = esper.World()
@classmethod
def create_controller(cls): cls.ctrl = game.Controller()
@classmethod
def create_window(cls):
cls.window = game.Window(
cls.settings.window_width, cls.settings.window_height
)
@classmethod
def create_consoles(cls):
cls.con = game.Console(window_w(),window_h())
@classmethod
def create_data(cls): cls.data = game.GameData()
@classmethod
def create_map(cls, w, h):
cls.map = tilemap.TileMap(w,h)
@classmethod
def create_clock(cls): cls.clock = game.Clock()
@classmethod
def create_updater(cls): cls.update = game.Update()
@classmethod
def create_view(cls): cls.view = game.View(view_port_w(),view_port_h(), ROOMW,ROOMH)
@classmethod
def create_log(cls): cls.log = game.MessageLog()
@classmethod
def create_savedGame(cls): cls.savedGame = game.SavedGame()
@classmethod
def create_player(cls, sx,sy):
cls.pc = player.chargen(sx, sy)
@classmethod
def create_processors(cls):
#Processor class, priority (higher = processed first)
cls.world.add_processor(proc.MetersProcessor(), 101)
cls.world.add_processor(proc.StatusProcessor(), 100)
cls.world.add_processor(proc.UpkeepProcessor(), 93)
cls.world.add_processor(proc.FluidProcessor(), 92)
cls.world.add_processor(proc.FireProcessor(), 91)
cls.world.add_processor(proc.TimersProcessor(), 90)
# after all changes have been made
cls.world.add_processor(proc.GUIProcessor(), 71)
# AI function (entity turns)
cls.world.add_processor(proc.ActorsProcessor(), 50)
@classmethod
def create_perturn_managers(cls):
'''
constant, per-turn managers, ran each turn
'''
#ran at beginning of turn
# (No managers run at beginning of turn currently.)
#ran at end of turn (before player turn -- player turn is the very final thing to occur on any given turn)
cls.et_managers.update({'sounds' : managers.Manager_SoundsHeard()})
cls.et_managers.update({'sights' : managers.Manager_SightsSeen()})
@classmethod
def create_const_managers(cls):
'''
constant managers, manually ran
'''
## cls.c_managers.update({'sights' : managers.Manager_SightsSeen()})
## cls.c_managers.update({'sounds' : managers.Manager_SoundsHeard()})
cls.c_managers.update({'events' : managers.Manager_Events()})
cls.c_managers.update({'lights' : managers.Manager_Lights()})
cls.c_managers.update({'fov' : managers.Manager_FOV()})
'''
WHAT DO WE DO WITH THESE MANAGERS????
FOV and ActionQueue is now a processor.... Events?????
Rogue.c_managers.update({'events' : managers.Manager_Events()})
'''
# EXPIRED: Environment class
#/Rogue
#----------------#
# Functions #
#----------------#
# global objects
def settings(): return Rogue.settings
# ECS
def get_slots(obj): # should only be used if absolutely necessary...
return set(getattr(obj, '__slots__', set()))
# world
def world(): return Rogue.world
# player
def pc(): return Rogue.pc
def is_pc(ent): return (ent==Rogue.pc)
# saved game
def playableJobs():
return entities.getJobs().items() #Rogue.savedGame.playableJobs
# log
def get_time(turn):
tt = turn + STARTING_TIME
day = 1 + tt // 86400
hour = (tt // 3600) % 24
minute = (tt // 60) % 60
second = tt % 60
return "D{},{:02d}:{:02d}:{:02d}".format(day, hour, minute, second)
def logNewEntry():
Rogue.log.drawNew()
def msg(txt, col=None):
if col is None: #default text color
col=COL['white']
Rogue.log.add(txt, str(get_time(get_turn())) )
def msg_clear():
clr=libtcod.console_new(msgs_w(), msgs_h())
libtcod.console_blit(clr, 0,0, msgs_w(),msgs_h(), con_game(), 0,0)
libtcod.console_delete(clr)
# game data
def dlvl(): return Rogue.data.dlvl() #current dungeon level of player
def level_up(): Rogue.data.dlvl_update(Rogue.data.dlvl() + 1)
def level_down(): Rogue.data.dlvl_update(Rogue.data.dlvl() - 1)
def level_set(lv): Rogue.data.dlvl_update(lv)
# clock
def turn_pass(): Rogue.clock.turn_pass()
def get_turn(): return Rogue.clock.turn
# view
def view_nudge(dx,dy): Rogue.view.nudge(dx,dy)
def view_nudge_towards(ent):Rogue.view.follow(ent)
def view_center(ent):
pos=Rogue.world.component_for_entity(ent, cmp.Position)
Rogue.view.center(pos.x, pos.y)
def view_center_player():
pos=Rogue.world.component_for_entity(Rogue.pc, cmp.Position)
Rogue.view.center(pos.x, pos.y)
def view_center_coords(x,y):Rogue.view.center(x,y)
def view_x(): return Rogue.view.x
def view_y(): return Rogue.view.y
def view_w(): return Rogue.view.w
def view_h(): return Rogue.view.h
def view_max_x(): return ROOMW - Rogue.view.w #constraints on view panning
def view_max_y(): return ROOMH - Rogue.view.h
def fixedViewMode_toggle(): Rogue.view.fixed_mode_toggle()
# map
def map(z = -99):
# TODO: code for loading / unloading levels into the map
if z == -99:
z = dlvl()
if dlvl() == z:
return Rogue.map
else:
# unload current map from Rogue.map into the levels dict
# load new map into Rogue.map from the levels dict
# update dlvl
level_set(z)
return Rogue.map
def tile_get(x,y): return Rogue.map.get_char(x,y)
def tile_change(x,y,char):
updateNeeded=Rogue.map.tile_change(x,y,char)
if updateNeeded:
update_all_fovmaps()
def map_reset_lighting(): Rogue.map.grid_lighting_init()
def tile_lighten(x,y,value):Rogue.map.tile_lighten(x,y,value)
def tile_darken(x,y,value): Rogue.map.tile_darken(x,y,value)
def tile_set_light_value(x,y,value):Rogue.map.tile_set_light_value(x,y,value)
def get_light_value(x,y): return Rogue.map.get_light_value(x,y)
##def map_generate(Map,level): levels.generate(Map,level) #OLD OBSELETE
def identify_symbol_at(x,y):
asci = libtcod.console_get_char(0, getx(x),gety(y))
char = "{} ".format(chr(asci)) if (asci < 128 and not asci==32) else ""
desc="__IDENTIFY UNIMPLEMENTED__" #IDENTIFIER.get(asci,"???")
return "{}{}".format(char, desc)
def grid_remove(ent): #remove thing from grid of things
return Rogue.map.remove_thing(ent)
def grid_insert(ent): #add thing to the grid of things
return Rogue.map.add_thing(ent)
def grid_fluids_insert(obj): Rogue.map.grid_fluids[obj.x][obj.y].append(obj)
def grid_fluids_remove(obj): Rogue.map.grid_fluids[obj.x][obj.y].remove(obj)
# updater
def update_base(): Rogue.update.base()
#def update_pcfov(): Rogue.update.pcfov()
def update_game(): Rogue.update.game()
def update_msg(): Rogue.update.msg()
def update_hud(): Rogue.update.hud()
def update_final(): Rogue.update.final()
#apply all updates if applicable
def game_update(): Rogue.update.update()
# consoles
def con_game(): return Rogue.con.game
def con_final(): return Rogue.con.final
# controller
def end(): Rogue.ctrl.end()
def game_state(): return Rogue.ctrl.state
def game_is_running(): return Rogue.ctrl.isRunning
def game_set_state(state="normal"):
print("$---Game State changed from {} to {}".format(game_state(), state))
Rogue.ctrl.set_state(state)
def game_resume_state(): return Rogue.ctrl.resume_state
def set_resume_state(state):Rogue.ctrl.set_resume_state(state)
# window
def window_w(): return Rogue.window.root.w
def window_h(): return Rogue.window.root.h
def view_port_x(): return Rogue.window.scene.x
def view_port_y(): return Rogue.window.scene.y
def view_port_w(): return Rogue.window.scene.w
def view_port_h(): return Rogue.window.scene.h
def hud_x(): return Rogue.window.hud.x
def hud_y(): return Rogue.window.hud.y
def hud_w(): return Rogue.window.hud.w
def hud_h(): return Rogue.window.hud.h
def msgs_x(): return Rogue.window.msgs.x
def msgs_y(): return Rogue.window.msgs.y
def msgs_w(): return Rogue.window.msgs.w
def msgs_h(): return Rogue.window.msgs.h
def set_hud_left(): Rogue.window.set_hud_left()
def set_hud_right(): Rogue.window.set_hud_right()
# functions from modules #
# orangio
def init_keyBindings():
IO.init_keyBindings()
# game
def dbox(x,y,w,h,text='', wrap=True,border=0,margin=0,con=-1,disp='poly'):
'''
x,y,w,h location and size
text display string
border border style. None = No border
wrap whether to use automatic word wrapping
margin inside-the-box text padding on top and sides
con console on which to blit textbox, should never be 0
-1 (default) : draw to con_game()
disp display mode: 'poly','mono'
'''
if con==-1: con=con_game()
misc.dbox(x,y,w,h,text, wrap=wrap,border=border,margin=margin,con=con,disp=disp)
def makeConBox(w,h,text):
con = libtcod.console_new(w,h)
dbox(0,0, w,h, text, con=con, wrap=False,disp='mono')
return con
# printing functions #
#@debug.printr
def refresh(): # final to root and flush
libtcod.console_blit(con_final(), 0,0,window_w(),window_h(), 0, 0,0)
libtcod.console_flush()
#@debug.printr
def render_gameArea(pc) :
con = Rogue.map.render_gameArea(pc, view_x(),view_y(),view_w(),view_h() )
libtcod.console_clear(con_game()) # WHY ARE WE DOING THIS?
libtcod.console_blit(con, view_x(),view_y(),view_w(),view_h(),
con_game(), view_port_x(),view_port_y())
#@debug.printr
def render_hud(pc) :
con = misc.render_hud(hud_w(),hud_h(), pc, get_turn(), dlvl() )
libtcod.console_blit(con,0,0,0,0, con_game(),hud_x(),hud_y())
def clear_final():
libtcod.console_clear(con_final())
#@debug.printr
def blit_to_final(con,xs,ys, xdest=0,ydest=0): # window-sized blit to final
libtcod.console_blit(con, xs,ys,window_w(),window_h(),
con_final(), xdest,ydest)
#@debug.printr
def alert(text=""): # message that doesn't go into history
dbox(msgs_x(),msgs_y(),msgs_w(),msgs_h(),text,wrap=False,border=None,con=con_final())
refresh()
# Error checking
class ComponentException(Exception):
''' raised when an entity lacks an expected component. '''
pass
def asserte(ent, condition, errorstring=""): # "ASSERT Entity"
''' ASSERT condition condition satisifed, else raise error
and print ID info about the Entity ent.'''
if not condition:
# name
if world.has_component(ent, cmp.Name):
entname="with name '{}'".format(
world.component_for_entity(ent, cmp.Name).name)
else:
entname="<NO NAME>"
# position
if world.has_component(ent, cmp.Position):
entposx=world.component_for_entity(ent, cmp.Position).x
entposy=world.component_for_entity(ent, cmp.Position).y
else:
entposx = entposy = -1
# message
print("ERROR: rogue.py: function getms: entity {e} {n} at pos ({x},{y}) {err}".format(
e=ent, n=entname, x=entposx, y=entposy, err=errorstring))
raise ComponentException
# end def
# "Fun"ctions #
def around(i): # round with an added constant to nudge values ~0.5 up to 1 (attempt to get past some rounding errors)
return round(i + 0.00001)
def sign(n):
if n>0: return 1
if n<0: return -1
return 0
# tilemap
def thingat(x,y): return Rogue.map.thingat(x,y) #Thing object
def thingsat(x,y): return Rogue.map.thingsat(x,y) #list
def inanat(x,y): return Rogue.map.inanat(x,y) #inanimate Thing at
def monat (x,y): return Rogue.map.monat(x,y) #monster at
def solidat(x,y): return Rogue.map.solidat(x,y) #solid Thing at
def wallat(x,y): return (not Rogue.map.get_nrg_cost_enter(x,y) ) #tile wall
def fluidsat(x,y): return Rogue.et_managers['fluids'].fluidsat(x,y) #list
def lightsat(x,y): return Rogue.map.lightsat(x,y) #list
def fireat(x,y): return False #Rogue.et_managers['fire'].fireat(x,y)
def cost_enter(x,y): return Rogue.map.get_nrg_cost_enter(x,y)
def cost_leave(x,y): return Rogue.map.get_nrg_cost_leave(x,y)
def cost_move(xf,yf,xt,yt,data):
return Rogue.map.path_get_cost_movement(xf,yf,xt,yt,data)
def is_in_grid_x(x): return (x>=0 and x<ROOMW)
def is_in_grid_y(y): return (y>=0 and y<ROOMH)
def is_in_grid(x,y): return (x>=0 and x<ROOMW and y>=0 and y<ROOMH)
def in_range(x1,y1,x2,y2,Range): return (maths.dist(x1,y1, x2,y2) <= Range + .34)
# view
def getx(x): return x + view_port_x() - view_x()
def gety(y): return y + view_port_y() - view_y()
def mapx(x): return x - view_port_x() + view_x()
def mapy(y): return y - view_port_y() + view_y()
# terraforming
def dig(x,y):
#dig a hole in the floor if no solids here
#else dig the wall out
#use rogue's tile_change func so we update all FOVmaps
tile_change(x,y,FLOOR)
def singe(x,y): #burn tile
if Rogue.map.get_char(x,y) == FUNGUS:
tile_change(x,y,FLOOR)
def wind_force():
return 0 # TEMPORARY, TODO: create wind processor
def wind_direction():
return (1,0,) # TEMPORARY, TODO: create wind processor
# component functions #
# NOTE: generally better to call directly and save the function call overhead
# Thus we should not really use these functions...
def get(ent, component): #return an entity's component
return Rogue.world.component_for_entity(ent, component)
def has(ent, component): #return whether entity has component
return Rogue.world.has_component(ent, component)
def match(component):
return Rogue.world.get_component(component)
def matchx(*components):
return Rogue.world.get_components(components)
return True
def copyflags(toEnt,fromEnt): #use this to set an object's flags to that of another object.
for flag in Rogue.world.component_for_entity(fromEnt, cmp.Flags).flags:
make(toEnt, flag)
# duplicate component functions -- create copy(s) of a component
def dupCmpMeters(meters):
newMeters = cmp.Meters()
newMeters.temp = meters.temp
newMeters.rads = meters.rads
newMeters.sick = meters.sick
newMeters.expo = meters.expo
newMeters.pain = meters.pain
newMeters.fear = meters.fear
newMeters.bleed = meters.bleed
newMeters.rust = meters.rust
newMeters.rot = meters.rot
newMeters.wet = meters.wet
return newMeters
# entity functions #
# getms: GET Modified Statistic (base stat + modifiers (permanent and conditional))
def getms(ent, _var): # NOTE: must set the DIRTY_STATS flag to true whenever any stats or stat modifiers change in any way! Otherwise the function will return an old value!
world=Rogue.world
asserte(ent,world.has_component(ent,cmp.ModdedStats),"has no ModdedStats component.")
if on(ent, DIRTY_STATS): # dirty; re-calculate the stats first.
makenot(ent, DIRTY_STATS) # make sure we don't get caught in infinite loop...
modded=_update_stats(ent)
return modded.__dict__[_var]
return Rogue.world.component_for_entity(ent, cmp.ModdedStats).__dict__[_var]
def getbase(ent, _var): # get Base statistic
return Rogue.world.component_for_entity(ent, cmp.Stats).__dict__[_var]
# SET Stat -- set stat stat to value val set base stat
def sets(ent, stat, val):
Rogue.world.component_for_entity(ent, cmp.Stats).__dict__[stat] = val
make(ent, DIRTY_STATS)
# ALTer Stat -- change stat stat by val value
def alts(ent, stat, val):
Rogue.world.component_for_entity(ent, cmp.Stats).__dict__[stat] += val
make(ent, DIRTY_STATS)
def setAP(ent, val):
actor=Rogue.world.component_for_entity(ent, cmp.Actor)
actor.ap = val
def spendAP(ent, amt):
actor=Rogue.world.component_for_entity(ent, cmp.Actor)
actor.ap = actor.ap - amt
# skills
def getskill(ent, skill): # return skill level in a given skill
assert(Rogue.world.has_component(ent, cmp.Skills))
skills = Rogue.world.component_for_entity(ent, cmp.Skills)
return _getskill(skills.skills.get(skill, 0))
def getskillxp(ent, skill): # return skill experience in a given skill
assert(Rogue.world.has_component(ent, cmp.Skills))
skills = Rogue.world.component_for_entity(ent, cmp.Skills)
return skills.skills.get(skill, 0)
def _getskill(exp): return exp // EXP_LEVEL
def setskill(ent, skill, lvl): # set skill level
assert(Rogue.world.has_component(ent, cmp.Skills))
skills = Rogue.world.component_for_entity(ent, cmp.Skills)
skills.skills[skill] = lvl*EXP_LEVEL
make(ent,DIRTY_STATS)
def train(ent, skill, pts): # train (improve) skill
# diminishing returns on skill gainz
pts = pts - getskill(ent)*EXP_DIMINISH_RATE
# train one level at a time
if pts > 0:
make(ent,DIRTY_STATS)
skills = Rogue.world.component_for_entity(ent, cmp.Skills)
__train(skills, skill, pts)
def __train(skills, skill, pts):
if skills.skills[skill] >= MAX_LEVEL*EXP_LEVEL:
skills.skills[skill] = MAX_LEVEL*EXP_LEVEL
return
skills.skills[skill] = skills.skills.get(skill, 0) + min(pts,EXP_LEVEL)
pts = pts - EXP_LEVEL - EXP_DIMINISH_RATE
if pts > 0:
__train(ent, skill, pts)
def forget(ent, skill, pts): # lose skill experience
assert(Rogue.world.has_component(ent, cmp.Skills))
skills = Rogue.world.component_for_entity(ent, cmp.Skills)
skills.skills[skill] = max(0, skills.skills.get(skill, pts) - pts)
make(ent,DIRTY_STATS)
# flags
def on(ent, flag):
return flag in Rogue.world.component_for_entity(ent, cmp.Flags).flags
def make(ent, flag):
Rogue.world.component_for_entity(ent, cmp.Flags).flags.add(flag)
def makenot(ent, flag):
Rogue.world.component_for_entity(ent, cmp.Flags).flags.remove(flag)
#
##def has_equip(obj,item):
## return item in Rogue.world.component_for_entity(obj, )
def give(ent,item):
if on(item,FIRE):
burn(ent, FIRE_BURN)
cooldown(item)
Rogue.world.component_for_entity(ent, cmp.Inventory).data.append(item)
def take(ent,item):
Rogue.world.component_for_entity(ent, cmp.Inventory).data.remove(item)
def mutate(ent):
# TODO: do mutation
mutable = Rogue.world.component_for_entity(ent, cmp.Mutable)
pos = Rogue.world.component_for_entity(ent, cmp.Position)
entn = Rogue.world.component_for_entity(ent, cmp.Name)
# TODO: message based on mutation (i.e. "{t}{n} grew an arm!") Is this dumb?
event_sight(pos.x,pos.y,"{t}{n} mutated!".format(t=entn.title,n=entn.name))
def has_sight(ent):
if (Rogue.world.has_component(ent, cmp.SenseSight) and not on(ent,BLIND)):
return True
else:
return False
def port(ent,x,y): # move thing to absolute location, update grid and FOV
grid_remove(ent)
pos = Rogue.world.component_for_entity(ent, cmp.Position)
pos.x=x; pos.y=y;
grid_insert(ent)
update_fov(ent)
if Rogue.world.has_component(ent, cmp.LightSource):
compo = Rogue.world.component_for_entity(ent, cmp.LightSource)
compo.light.reposition(x, y)
def drop(ent,item,dx=0,dy=0): #remove item from ent's inventory, place it
take(ent,item) #on ground nearby ent.
itempos=Rogue.world.component_for_entity(item, cmp.Position)
entpos=Rogue.world.component_for_entity(ent, cmp.Position)
itempos.x=entpos.x + dx
itempos.y=entpos.y + dy
grid_insert(ent)
def givehp(ent,val=9999):
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.hp = min(getms(ent, 'hpmax'), stats.hp + val)
make(ent,DIRTY_STATS)
def givemp(ent,val=9999):
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.mp = min(getms(ent, 'mpmax'), stats.mp + val)
make(ent,DIRTY_STATS)
def caphp(ent): # does not make stats dirty! Doing so is a huge glitch!
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.hp = min(stats.hp, getms(ent,'hpmax'))
def capmp(ent): # does not make stats dirty! Doing so is a huge glitch!
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.mp = min(stats.mp, getms(ent,'mpmax'))
# these aren't tested, nor are they well thought-out
def knock(ent, amt): # apply force to an entity
mass = getms(ent, 'mass')
bal = getms(ent, 'bal')
effmass = mass * bal / BAL_MASS_MULT # effective mass
dmg = amt // effmass
stagger(ent, dmg)
def stagger(ent, dmg): # reduce balance by dmg
if Rogue.world.has_component(ent, cmp.StatusOffBalance):
compo=Rogue.world.component_for_entity(ent, cmp.StatusOffBalance)
dmg = dmg + compo.quality
set_status(ent, cmp.StatusOffBalance(
t = min(8, 1 + dmg//4), q=dmg) )
#damage hp
def damage(ent, dmg: int):
## assert isinstance(dmg, int)
if dmg <= 0: return
make(ent,DIRTY_STATS)
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.hp -= dmg
if stats.hp <= 0:
kill(ent)
#damage mp (stamina)
def sap(ent, dmg: int, exhaustOnZero=True):
## assert isinstance(dmg, int)
## print('sapping ', dmg)
if dmg < 0: return
make(ent,DIRTY_STATS)
stats = Rogue.world.component_for_entity(ent, cmp.Stats)
stats.mp = int(stats.mp - dmg)
if stats.mp <= 0:
if exhaustOnZero:
exhaust(ent) # TODO
else:
stats.mp = 0
def exhaust(ent):
print('ent {} exhausted.'.format(ent))
kill(ent)
# satiation / hydration
def feed(ent, sat, hyd): # add satiation / hydration w/ Digest status
if world.has_component(ent, cmp.StatusDigest):
compo = Rogue.world.component_for_entity(ent, cmp.StatusDigest)
compo.satiation += sat
compo.hydration += hyd
else:
world.add_component(ent, cmp.StatusDigest(cald, hydd))
def sate(ent, pts):
compo = Rogue.world.component_for_entity(ent, cmp.Body)
compo.satiation = min(compo.satiation + pts, compo.satiationMax)
#TODO: convert excess calories to fat(?) how/where should this be done?
def hydrate(ent, pts):
compo = Rogue.world.component_for_entity(ent, cmp.Body)
compo.hydration = min(compo.hydration + pts, compo.hydrationMax)
# elemental damage
def settemp(ent, temp):
Rogue.world.component_for_entity(ent, cmp.Meters).temp = temp
def burn(ent, dmg, maxTemp=999999):
return entities.burn(ent, dmg, maxTemp)
def cool(ent, dmg, minTemp=-300):
return entities.burn(ent, dmg, minTemp)
def bleed(ent, dmg):
return entities.bleed(ent, dmg)
def hurt(ent, dmg):
return entities.hurt(ent, dmg)
def disease(ent, dmg):
return entities.disease(ent, dmg)
def intoxicate(ent, dmg):
return entities.intoxicate(ent, dmg)
def irradiate(ent, dmg):
return entities.irradiate(ent, dmg)
def irritate(ent, dmg):
return entities.irritate(ent, dmg)
def exposure(ent, dmg):
return entities.exposure(ent, dmg)
def corrode(ent, dmg):
return entities.corrode(ent, dmg)
def cough(ent, dmg):
return entities.cough(ent, dmg)
def vomit(ent, dmg):
return entities.vomit(ent, dmg)
def electrify(ent, dmg):
return entities.electrify(ent, dmg)
def paralyze(ent, dur):
return entities.paralyze(ent, dur)
def wet(ent, g):
return entities.wet(ent, g)
def dirty(ent, g):
return entities.dirty(ent, g)
def mutate(ent):
return entities.mutate(ent)
def kill(ent): #remove a thing from the world
if on(ent, DEAD): return
world = Rogue.world
_type = world.component_for_entity(ent, cmp.Draw).char
if world.has_component(ent, cmp.DeathFunction): # call destroy function
world.component_for_entity(ent, cmp.DeathFunction).func(ent)
make(ent, DEAD)
clear_status_all(ent)
#drop inventory
if world.has_component(ent, cmp.Inventory):
for tt in world.component_for_entity(ent, cmp.Inventory).data:
drop(ent, tt)
#creatures
isCreature = world.has_component(ent, cmp.Creature)
if isCreature:
#create a corpse
if dice.roll(100) < entities.corpse_recurrence_percent[_type]:
create_corpse(ent)
#inanimate things
else:
#burn to ashes
if on(ent, FIRE):
mat = world.component_for_entity(ent, cmp.Material).flag
if (mat==MAT_FLESH
or mat==MAT_WOOD
or mat==MAT_FUNGUS
or mat==MAT_VEGGIE
or mat==MAT_LEATHER
):
create_ashes(ent)
#remove dead thing
if Rogue.world.has_component(ent, cmp.Position):
release_entity(ent)
# end def
def zombify(ent):
kill(ent) # temporary
def explosion(name, x, y, radius):
event_sight(x, y, "{n} explodes!".format(n=name))
# inreach | in reach | in_reach
# Calculate if your target is within your reach (melee range).
# Instead of calculating with a slow* sqrt func, we access this cute grid:
_REACHGRIDQUAD=[ #0==origin
[10,10,99,99,99,99,], # we only need 1 quadrant of the total
[8, 8, 9, 10,99,99,], # grid to calculate if you are in reach.
[6, 7, 7, 9, 10,99,],
[4, 5, 6, 7, 9, 99,],
[2, 3, 5, 7, 8, 10,],
[0, 2, 4, 6, 8, 10,],
]
# *not sure if this is actually faster, but it also gives me direct
# control over which tiles are covered by which reach values.
def inreach(x1,y1, x2,y2, reach):
''' reach is the stat //MULT_STATS but not divided by 2 '''
xd = abs(x2 - x1) # put the coordinates in the upper right quadrant ...
yd = -abs(y2 - y1) # ... to match with the quadrant grid above
if (xd > MAXREACH or yd < -MAXREACH): return False # max distance exceeded
reachNeeded = _REACHGRIDQUAD[MAXREACH+yd][xd]
if reach >= reachNeeded:
return True
return False
# end def
#----------------------#
# Events #
#----------------------#
def event_sight(x,y,text):
if not text: return
Rogue.c_managers['events'].add_sight(x,y,text)
def event_sound(x,y,data):
if (not data): return
volume,text1,text2=data
Rogue.c_managers['events'].add_sound(x,y,text1,text2,volume)
# TODO: differentiate between 'events' and 'sights' / 'sounds' which are for PC entity only (right???)
def listen_sights(ent): return Rogue.c_managers['events'].get_sights(ent)
def add_listener_sights(ent): Rogue.c_managers['events'].add_listener_sights(ent)
def remove_listener_sights(ent): Rogue.c_managers['events'].remove_listener_sights(ent)
def clear_listen_events_sights(ent):Rogue.c_managers['events'].clear_sights(ent)
def listen_sounds(ent): return Rogue.c_managers['events'].get_sounds(ent)
def add_listener_sounds(ent): Rogue.c_managers['events'].add_listener_sounds(ent)
def remove_listener_sounds(ent): Rogue.c_managers['events'].remove_listener_sounds(ent)
def clear_listen_events_sounds(ent):Rogue.c_managers['events'].clear_sounds(ent)
def clear_listeners(): Rogue.c_managers['events'].clear()
##def listen_sights(ent): return Rogue.c_managers['sights'].get_sights(ent)
##def add_listener_sights(ent): Rogue.c_managers['sights'].add_listener_sights(ent)
##def remove_listener_sights(ent): Rogue.c_managers['sights'].remove_listener_sights(ent)
##def clear_listen_events_sights(ent):Rogue.c_managers['sights'].clear_sights(ent)
##def listen_sounds(ent): return Rogue.c_managers['sounds'].get_sounds(ent)
##def add_listener_sounds(ent): Rogue.c_managers['sounds'].add_listener_sounds(ent)
##def remove_listener_sounds(ent): Rogue.c_managers['sounds'].remove_listener_sounds(ent)
##def clear_listen_events_sounds(ent):Rogue.c_managers['sounds'].clear_sounds(ent)
def pc_listen_sights(): # these listener things for PC might need some serious work...
pc=Rogue.pc
lis=listen_sights(pc)
if lis:
for ev in lis:
Rogue.c_managers['sights'].add(ev)
manager_sights_run()
def pc_listen_sounds():
pc=Rogue.pc
lis=listen_sounds(pc)
if lis:
for ev in lis:
Rogue.c_managers['sounds'].add(ev)
manager_sounds_run()
#----------------#
# FOV #
#----------------#
# TODO: test FOV for NPCs to make sure it works properly!!!
def update_fov(ent):
Rogue.c_managers['fov'].update(ent)
def run_fov_manager(ent):
Rogue.c_managers['fov'].run(ent)
def update_pcfov():
Rogue.c_managers['fov'].update(Rogue.pc)
def run_pcfov_manager():
Rogue.c_managers['fov'].run(Rogue.pc)
def fov_init(): # normal type FOV map init
#TODO: THIS CODE NEEDS TO BE UPDATED. ONLY MAKE AS MANY FOVMAPS AS NEEDED.
# Is it really worth it to change this?
fovMap=libtcod.map_new(ROOMW,ROOMH)
libtcod.map_copy(Rogue.map.fov_map,fovMap) # get properties from Map
return fovMap
#@debug.printr
def fov_compute(ent):
## print("computing fov for ent {}".format(ent))
pos = Rogue.world.component_for_entity(ent, cmp.Position)
senseSight = Rogue.world.component_for_entity(ent, cmp.SenseSight)
libtcod.map_compute_fov(
senseSight.fov_map, pos.x, pos.y, getms(ent, 'sight'),
light_walls = True, algo=libtcod.FOV_RESTRICTIVE
)
def update_fovmap_property(fovmap, x,y, value):
libtcod.map_set_properties( fovmap, x,y,value,True)
##def compute_fovs(): Rogue.c_managers['fov'].run()
# circular FOV function
def can_see(ent,x,y):
world = Rogue.world
if (get_light_value(x,y) == 0 and not on(ent,NVISION)):
return False
pos = world.component_for_entity(ent, cmp.Position)
senseSight = world.component_for_entity(ent, cmp.SenseSight)
return ( in_range(pos.x,pos.y, x,y, getms(ent, "sight")) #<- circle-ize
and libtcod.map_is_in_fov(senseSight.fov_map,x,y) )
#copies Map 's fov data to all creatures - only do this when needed
# also flag all creatures for updating their fov maps
def update_all_fovmaps():
for ent, compo in Rogue.world.get_component(cmp.SenseSight):
fovMap=compo.fov_map
libtcod.map_copy(Rogue.map.fov_map, fovMap)
update_fov(ent)
#******we should overhaul this FOV system!~*************
#creatures share fov_maps. There are a few fov_maps
#which have different properties like x-ray vision, etc.
#the only fov_maps we have should all be unique. Would save time.
#update_all_fovmaps only updates these unique maps.
#this would probably be much better, I should do this for sure.
#----------------#
# Paths #
#----------------#
def can_hear(ent, x,y, volume):
world = Rogue.world
if ( on(ent,DEAD) or (not world.has_component(ent, cmp.SenseHearing)) ):
return False
pos = world.component_for_entity(ent, cmp.Position)
senseHearing = world.component_for_entity(ent, cmp.SenseHearing)
dist=maths.dist(pos.x, pos.y, x, y)
maxHearDist = volume * senseHearing.hearing / AVG_HEARING
if (pos.x == x and pos.y == y): return (0,0,maxHearDist,)
if dist > maxHearDist: return False
# calculate a path
path=path_init_sound()
path_compute(path, pos.x,pos.y, x,y)
pathSize=libtcod.path_size(path)
if dist >= 2:
semifinal=libtcod.path_get(path, 0)
xf,yf=semifinal
dx=xf - pos.x
dy=yf - pos.y
else:
dx=0
dy=0
path_destroy(path)
loudness=(maxHearDist - pathSize - (pathSize - dist))
if loudness > 0:
return (dx,dy,loudness)
def path_init_movement():
pathData=0
return Rogue.map.path_new_movement(pathData)
def path_init_sound():
pathData=0
return Rogue.map.path_new_sound(pathData)
def path_compute(path, xfrom,yfrom, xto,yto):
libtcod.path_compute(path, xfrom,yfrom, xto,yto)
def path_destroy(path):
libtcod.path_delete(path)
def path_step(path):
x,y=libtcod.path_walk(path, True)
return x,y
#----------------#
# Things #
#----------------#
def register_entity(ent): # NOTE!! this no longer adds to grid.
create_moddedStats(ent) # is there a place this would belong better?
make(ent,DIRTY_STATS)
def release_entity(ent):
# do a bunch of precautionary stuff / remove entity from registers ...
remove_listener_sights(ent)
remove_listener_sounds(ent)
grid_remove(ent)
# esper
delete_entity(ent)
def delete_entity(ent):
Rogue.world.delete_entity(ent)
def create_stuff(ID, x,y): # create & register an item from stuff list
tt = entities.create_stuff(ID, x,y)
register_entity(tt)
return tt
def create_rawmat(ID, x,y): # create & register an item from raw materials
tt = entities.create_rawmat(ID, x,y)
register_entity(tt)
return tt
def create_entity():
return Rogue.world.create_entity()
##def create_entity_flagset(): # create an entity with a flagset
## ent = Rogue.world.create_entity()
## Rogue.world.add_component(ent, cmp.Flags) #flagset
## return ent
# creature/monsters #
def create_monster(typ,x,y,col,mutate=3): #init from entities.py
'''
call this to create a monster from the bestiary
TODO: figure out what to do to create monsters/creatures in general
code for that is not complete...
'''
if monat(x,y):
return None #tile is occupied by a creature already.
ent = entities.create_monster(typ,x,y,col,mutate)
## init_inventory(monst, monst.stats.get('carry')) # do this in create_monster
givehp(ent)
register_entity(ent)
fov_init(ent)
grid_insert(ent)
return ent
def create_corpse(ent):
corpse = entities.convertTo_corpse(ent)
## register_entity(corpse)
return corpse
def create_ashes(ent):
ashes = entities.convertTo_ashes(ent)
## register_entity(ashes)
return ashes
def convert_to_creature(ent, job=None, faction=None, species=None):
''' try to give entity Creature component, return success '''
pos = Rogue.world.component_for_entity(ent, cmp.Position)
if monat(pos.x,pos.y):