codetower/ct_lib.py

564 lines
23 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import bge # Blender Game Engine (UPBGE)
import aud # Sounds
import threading # Multithreading
import trace
import sys
import time
import math
import mathutils
import random
###############################################################################
# ct_lib.py
# @title: User library
# @project: CodeTower
# @lang: fr,en
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
# @copyright: Copyright (C) 2022 Philippe Roy
# @license: GNU GPL
#
# This game is a tower defense coding game. The towers are driven by Python code.
# Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
#
# Commands trigged by button : cmd_*
# Commands trigged by 3D scene objects : scn_*
# Commands trigged by user (student or map designer) : ct_*
# 3D scene manipulation : manip_*
#
###############################################################################
scene = bge.logic.getCurrentScene()
# Colors
tower_purple = [0.202, 0.114, 0.521,1]
tower_turquoise = [0.051, 0.270, 0.279,1]
tower_magenta = [0.799, 0.005, 0.314,1]
tower_orange = [0.799, 0.130, 0.063,1]
tower_yellow = [0.799, 0.617, 0.021, 1]
tower_green = [0.246, 0.687, 0.078, 1]
tower_red = [0.799, 0.031, 0.038, 1]
tower_blue = [0.127, 0.456, 1.000, 1]
tower_black = [0, 0, 0, 1]
ray_yellow = [0.799, 0.617, 0.021, 1] # [0.8, 0.619, 0.021])
ray_blue = [0.127, 0.456, 1.000, 1]
ray_black = [0, 0, 0, 1]
color_kaykit_black = [0.019, 0.032, 0.037, 1]
# Sounds
audiodev = aud.Device()
snd_build = aud.Sound('asset/sounds/build.wav')
sndbuff_build = aud.Sound.cache(snd_build)
snd_archer = aud.Sound('asset/sounds/archer.wav')
sndbuff_archer = aud.Sound.cache(snd_archer)
snd_mage = aud.Sound('asset/sounds/mage.wav')
sndbuff_mage = aud.Sound.cache(snd_mage)
snd_life = aud.Sound('asset/sounds/life.ogg')
sndbuff_life = aud.Sound.cache(snd_life)
###############################################################################
# Données générales
###############################################################################
def ct_level_current():
return scene.objects['Points']['level']
def ct_level():
return scene.objects['Points']['level_max']
###############################################################################
# Méthode kill pour les tâches (threads)
###############################################################################
class thread_with_trace(threading.Thread):
def __init__(self, *args, **keywords):
threading.Thread.__init__(self, *args, **keywords)
self.killed = False
def start(self):
self.__run_backup = self.run
self.run = self.__run
threading.Thread.start(self)
def __run(self):
sys.settrace(self.globaltrace)
self.__run_backup()
self.run = self.__run_backup
def globaltrace(self, frame, event, arg):
if event == 'call':
return self.localtrace
else:
return None
def localtrace(self, frame, event, arg):
if self.killed:
if event == 'line':
raise SystemExit()
return self.localtrace
def kill(self):
self.killed = True
###############################################################################
# Start et stop des tâches (threads)
###############################################################################
def thread_start(threads, type_txt, fct):
threads.append(thread_with_trace(target = fct))
threads[len(threads)-1].start()
print ("Thread",type_txt, "#", len(threads)-1, "open.")
def thread_stop(threads, type_txt):
i=0
zombie_flag=False
for t in threads:
if not t.is_alive():
print ("Thread",type_txt, "#",i,"closed.")
else:
print ("Thread",type_txt, "#",i,"still open ...")
t.kill()
t.join()
if not t.is_alive():
print ("Thread",type_txt, "#",i,"killed.")
else:
print ("Thread",type_txt, "#",i,"zombie...")
zombie_flag=True
i +=1
if zombie_flag==False:
print ("All threads",type_txt, "are closed.")
return True
else:
print ("There are zombies threads",type_txt, ".")
return False
###############################################################################
# Vagues (minions)
###############################################################################
# Minion caracteristics : category (class), level, hp, speed, armor, bounty, lifes_damage
# Minion 3d object : body (male,female,old, ...), variante (A,B,C,D, ...), level
# Création d'un minion
def ct_minion(x,y,cat,level):
category=cat+"-lv"+str(level)
minion_3d= scene.objects['Terrain']['minion_3d']
body = random.choice(minion_3d[category][0])+"_"+random.choice(minion_3d[category][1])+"_"+random.choice(minion_3d[category][2])
ct_minion_details(x,y,cat,level,body)
# Création d'un minion détaillée
def ct_minion_details(x,y,cat,level,body="Knight_m_A_common"):
category=cat+"-lv"+str(level)
# Pause
while scene.objects['Terrain']['run'] == False:
time.sleep(0.01)
# Object 3D
minion= scene.addObject(body, scene.objects['Terrain'])
minion.worldScale=[0.25,0.25,0.25]
minion.worldPosition=[x,y,0.1]
scene.objects['Terrain']['idm']=scene.objects['Terrain']['idm']+1
minion.name="wm("+str(scene.objects['Terrain']['idm'])+")" # Wave minion (wm), identifier minion (idm)
scene.objects['Points']['minions'] +=1
scene.objects['Points']['minions_run'] +=1
# Caracteristics
minion_carac= scene.objects['Terrain']['minion_carac']
minion['cat']=minion_carac[category][0]
minion['level']=minion_carac[category][1]
minion['hp']=minion_carac[category][2]
minion['speed']=minion_carac[category][3]
minion['speed_base']=minion_carac[category][3]
minion['armor']=minion_carac[category][4]
minion['bounty']=minion_carac[category][5]
minion['lifes_damage']=minion_carac[category][6]
minion['buff']=[]
minion['resist']=[]
# Actuator Steering
minion.actuators['Steering'].navmesh=scene.objects[scene.objects['Terrain']['navmesh']]
minion.actuators['Steering'].target=scene.objects[scene.objects['Terrain']['endtile']]
minion.actuators['Steering'].distance=0.5
minion.actuators['Steering'].velocity=minion['speed_base']*scene.objects['Terrain']['speed']
# Destruction d'un minion
def scn_minion_dead(cont):
obj = cont.owner
scene.objects['Points']['minions'] -=1
scene.objects['Points']['minions_run'] -=1
scene.objects['Points']['kills'] +=1
scene.objects['Points']['coins']= scene.objects['Points']['coins']+obj['bounty']
obj.endObject()
###############################################################################
# Spells
###############################################################################
# Buff/debuff Minion
def scn_minion_affect(cont):
obj = cont.owner
# print (obj.name, obj['buff'])
slow_state=False
# Etats actif
for debuff_i in obj['buff']:
if debuff_i[1] <= 0:
obj['buff'].remove(debuff_i)
continue
if debuff_i[0] == "slow":
slow_state=True
debuff_i[1] -= scene.objects['Terrain']['speed']
# Effets
if slow_state:
obj.actuators['Steering'].velocity =(obj['speed_base']*scene.objects['Terrain']['speed'])/2
else:
obj.actuators['Steering'].velocity = obj['speed_base']*scene.objects['Terrain']['speed']
###############################################################################
# Tours
###############################################################################
# Tower caracteristics : category (class), damage, speed, range
# Création d'une tour
def ct_build(x,y, cat='Archer tower', tower_name="Tower", color=tower_purple, tower_3d="square-A"):
tower_minion_3d= scene.objects['Terrain']['tower_minion_3d']
if cat=='Archer tower': # Archer
category="Archer-lv1"
if cat=='Mage tower': # Mage
category="Mage-lv1"
body = random.choice(tower_minion_3d[category][0])+"_"+random.choice(tower_minion_3d[category][1])+"_"+random.choice(tower_minion_3d[category][2])
ct_build_details(x,y, cat, tower_name, color, tower_3d, body)
# Création d'une tour détaillée
def ct_build_details(x,y, cat='Archer tower', tower_name="Tower", color=tower_purple, tower_3d="square-A", body="Archer_m_A_common"):
# Vérification de la place
if [x,y] in scene.objects['Terrain']['scene_tile_noncontruct'] or [x,y] in scene.objects['Terrain']['scene_tile_tower']:
return False
# Vérification du niveau
scene.objects['Points']['level']= scene.objects['Points']['level'] + 1
if scene.objects['Points']['level'] > scene.objects['Points']['level_max'] :
tour= scene.addObject("Tower_error", scene.objects['Terrain'])
tour.worldPosition=[x,y,0.2]
tour.worldScale=[1,1,1]
scene.objects['Terrain']['scene_tile_tower'].append([x,y])
return False
# Objets 3D
tour= scene.addObject('Tower-'+tower_3d, scene.objects['Terrain'])
tour.color = color
tour.worldPosition=[x,y,0.2]
tour.worldScale=[1,1,1]
scene.objects['Terrain']['scene_tile_tower'].append([x,y])
tower_minion= scene.addObject(body, scene.objects['Terrain'])
tower_minion["type_towerminion"]=False
tower_minion.name="tm("+str(x)+','+str(y)+")" # Tower minion (tm)
tower_minion.worldPosition=[x,y,1]
tower_minion.worldScale=[0.25,0.25,0.25]
# Sounds
audiodev.play(sndbuff_build)
# Caracteristics
tower_carac= scene.objects['Terrain']['tower_carac']
tour['cat']=tower_carac[cat][0]
tour['tower_name']=tower_name
tour['xp']=0
tour['lvl_current']=1
tour['lvl']=1
tour['damage']=tower_carac[cat][1]
tour['speed']=tower_carac[cat][2]
tour['range']=tower_carac[cat][3]
tour['techno']=[]
tour['cast']="slow"
tour['cast_duration']=2
# Capteur Near
tour.sensors['Near'].distance=tour['range']
tour.sensors['Near'].skippedTicks =round(1/(tour['speed']*scene.objects['Terrain']['speed']))
return True
# Suppression d'une tour
def ct_remove(x,y):
for obj_i in scene.objects:
if "type_tower" in obj_i.getPropertyNames():
if x == obj_i.worldPosition.x and y == obj_i.worldPosition.y:
scene.objects["tm("+str(round(obj_i.worldPosition.x))+','+str(round(obj_i.worldPosition.y))+")"].endObject()
obj_i.endObject()
scene.objects['Points']['level']= scene.objects['Points']['level'] - 1
# Réaction d'une tour
def scn_tower_near(cont):
obj = cont.owner
sensor = obj.sensors['Near']
# Tir
# FIXME: tir sur le plus avancé
if sensor.positive and len(sensor.hitObjectList)>0 and scene.objects['Terrain']['run']==True :
target=sensor.hitObjectList[0]
# Orientation du tower minion
towerminion="tm("+str(round(obj.worldPosition.x))+','+str(round(obj.worldPosition.y))+")"
angle =math.atan((target.worldPosition.y-obj.worldPosition.y)/(target.worldPosition.x-obj.worldPosition.x))
if target.worldPosition.x>obj.worldPosition.x:
angle2=math.pi/2+angle-scene.objects[towerminion].worldOrientation.to_euler().z
angle3=angle
else:
angle2=math.pi+math.pi/2+angle-scene.objects[towerminion].worldOrientation.to_euler().z
angle3=math.pi+angle
scene.objects[towerminion].applyRotation((0, 0, angle2), False)
# Bullet (3d object) (vitesse lente) # -> tendance au plantage
# if scene.objects['Terrain']['speed']<1:
# bullet= scene.addObject("Bullet", scene.objects['Terrain'])
# bullet.mass=0.001 # bullet.applyForce=((0,0,9.81),True)
# bullet.worldPosition=[obj.worldPosition.x,obj.worldPosition.y,1.5]
# bullet.worldScale=[0.75,0.75,0.75]
# # bullet.worldScale=[0.5,0.5,0.5]
# bullet.worldLinearVelocity.x = (target.worldPosition.x-bullet.worldPosition.x)*bullet['velocity']
# bullet.worldLinearVelocity.y= (target.worldPosition.y-bullet.worldPosition.y)*bullet['velocity']
# bullet.worldLinearVelocity.z = (target.worldPosition.z+0.1-bullet.worldPosition.z)*bullet['velocity']
# Sounds
if obj['cat']=="Archer tower":
audiodev.play(sndbuff_archer)
if obj['cat']=="Mage tower":
audiodev.play(sndbuff_mage)
# Ligne (drawLine) (vitesse rapide)
# if scene.objects['Terrain']['speed']>=1:
if scene.objects['Terrain']['speed']<10: # Pas d'animation à 10 -> plantage
# Archer (tir de flêche)
if obj['cat']=="Archer tower":
if target.name in scene.objects:
scene.objects['Terrain']['draw_process']=True
scene.objects['Terrain']['draw_list'].append([5, "arrow", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8],target.name, angle3, ray_black, 5])
# Cast zone
if obj['cat']=="Mage tower": # Mage (cast)
scene.objects['Terrain']['draw_process']=True
scene.objects['Terrain']['draw_list'].append([30, "cast", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8],ray_blue,30])
# Rayon
if obj['cat']=="Test":
if target.name in scene.objects:
scene.objects['Terrain']['draw_process']=True
scene.objects['Terrain']['draw_list'].append([5, "ray", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8], target.name, angle3, ray_yellow,5]) # Suivi du minion
# Dégats
target['hp'] = target['hp'] - obj['damage']
if target['hp']<=0:
target['dead']=True
# Cast (buff and debuff)
if obj['cat']=="Mage tower":
for target_i in sensor.hitObjectList:
target_i['buff'].append([obj['cast'], obj['cast_duration']])
###############################################################################
# Carte
###############################################################################
# Fin
def ct_map_end(x,y):
mapend= scene.addObject("Map_end", scene.objects['Terrain'])
mapend.worldPosition=[x,y,0.2]
mapend.worldScale=[0.25,0.25,0.25]
# Minion arrivé à la fin
def scn_map_end_near(cont):
obj = cont.owner
sensor = obj.sensors['Near']
for i in range (len(sensor.hitObjectList)) :
sensor.hitObjectList[i].endObject()
audiodev.play(sndbuff_life)
if scene.objects['Points']['lifes']>0:
scene.objects['Points']['lifes']= scene.objects['Points']['lifes']-sensor.hitObjectList[0]['lifes_damage']
scene.objects['Points']['minions_run'] -=1
# Drapeau de fin
def ct_map_endflag(x,y):
endflag= scene.addObject("Map_endflag", scene.objects['Terrain'])
endflag.worldPosition=[x,y,0.3]
endflag.worldScale=[0.3,0.3,0.3]
if round(x) == x :
if round(y) == y :
scene.objects['Terrain']['scene_tile_noncontruct'].append([x,y])
else:
scene.objects['Terrain']['scene_tile_noncontruct'].append([x,math.floor(y)])
scene.objects['Terrain']['scene_tile_noncontruct'].append([x,math.ceil(y)])
else:
if round(y) == y :
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.floor(x),y])
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.ceil(x),y])
else:
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.floor(x),math.floor(y)])
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.floor(x),math.ceil(y)])
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.ceil(x),math.floor(y)])
scene.objects['Terrain']['scene_tile_noncontruct'].append([math.ceil(x),math.ceil(y)])
###############################################################################
# Temporisation
###############################################################################
def ct_sleep (duration):
time.sleep(duration*(1/scene.objects['Terrain']['speed']))
# def ct_tempo (duration):
# scene.objects['Terrain']['delay_cmd']=0
# while scene.objects['Terrain']['delay_cmd']<duration*(1/scene.objects['Terrain']['speed']):
# # print("Temporization commands :",scene.objects['Terrain']['delay_cmd'])
# time.sleep(0.001)
# # pass
# def ct_tempo_wave (duration):
# scene.objects['Terrain']['delay_wave']=0
# while scene.objects['Terrain']['delay_wave']<duration*(1/scene.objects['Terrain']['speed']):
# # print("Temporization waves :",scene.objects['Terrain']['delay_wave'])
# time.sleep(0.001)
# # pass
# def ct_tempo_wave_trigger (duree):
# print ("delay wave ", scene.objects['Terrain']['delay_wave'])
###############################################################################
# Affichage
###############################################################################
# Texte du panel d'information
def ct_print (text):
# text_info (texte)
if text=="":
scene.objects['Text_info-1'].setVisible(False,False)
scene.objects['Text_info-2'].setVisible(False,False)
else:
lines_txt=texte.split("\n", 6)
for i in range (len(lines_txt),6):
lines_txt.append("")
scene.objects['Text_info-1'].setVisible(True,False)
scene.objects['Text_info-2'].setVisible(True,False)
scene.objects['Text_info-1']['Text']=lines_txt[0]+"\n"+lines_txt[1]+"\n"+lines_txt[2]
scene.objects['Text_info-2']['Text']=lines_txt[3]+"\n"+lines_txt[4]+"\n"+lines_txt[5]
# Texte de carte
def ct_map_text(text):
scene.objects['Map_text']['Text']=text
scene.objects['Map_text'].setVisible(True,False)
###############################################################################
# Dessin bas niveau (bge.render.drawLine)
###############################################################################
def circle (center, radius, color):
ang = 0.0
# ang_step = 0.1
ang_step = 0.2
while ang< 2 * math.pi:
x0 = center[0]+float(radius*math.cos(ang))
y0 = center[1]+float(radius*math.sin(ang))
x1 = center[0]+float(radius*math.cos(ang+ang_step))
y1 = center[1]+float(radius*math.sin(ang+ang_step))
bge.render.drawLine([x0,y0,center[2]],[x1,y1,center[2]],color)
ang += ang_step
# Affiche les draws en cours
# FIXME: tir sur le plus avancé
#
# Type de draw :
# arrow : [5, "arrow", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8],target.name, angle3, ray_yellow,5]
# cast : [30, "cast", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8], ray_blue,30]
# ray : [5, "ray", [obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8],[target.worldPosition.x, target.worldPosition.y, target.worldPosition.z], angle3, ray_yellow,5]
#
def scn_draw(cont):
obj = cont.owner
if obj.sensors['Draw'].positive==False:
return
if len(scene.objects['Terrain']['draw_list'])==0:
scene.objects['Terrain']['draw_process']=False
return
# Dépilage des draws à executer
for draw_cmd in scene.objects['Terrain']['draw_list']:
# Archer (tir de flêche)
if draw_cmd[1]=="arrow":
if draw_cmd[3] in scene.objects:
# x0 = draw_cmd[2][0]+0.25*(math.cos(draw_cmd[4]))
# y0 = draw_cmd[2][1]+0.25*(math.sin(draw_cmd[4]))
x0 = draw_cmd[2][0]
y0 = draw_cmd[2][1]
z0 = draw_cmd[2][2]
x1 = scene.objects[draw_cmd[3]].worldPosition.x
y1 = scene.objects[draw_cmd[3]].worldPosition.y
z1 = scene.objects[draw_cmd[3]].worldPosition.z-0.1 # ajustement -0.1
distance = math.sqrt((x1-x0)**2+(y1-y0)**2+(z1-z0)**2)
distance_xy = math.sqrt((x1-x0)**2+(y1-y0)**2)
distance_z = z1-z0
angle_z =math.atan((z1-z0)/(distance_xy))
angle_xy =math.atan((y1-y0)/(x1-x0))
step=distance_xy/(2+draw_cmd[6])
step_z=distance_z/(2+draw_cmd[6])
if x1>x0:
angle2=angle_xy
else:
angle2=math.pi+angle_xy
x2=x0+(((6-draw_cmd[0])*step)*(math.cos(angle2)))
y2=y0+(((6-draw_cmd[0])*step)*(math.sin(angle2)))
z2=z0-(((6-draw_cmd[0])*step_z)*(math.sin(angle_z)))
x3=x0+(((6-draw_cmd[0])*step+step)*(math.cos(angle2)))
y3=y0+(((6-draw_cmd[0])*step+step)*(math.sin(angle2)))
z3=z0-(((6-draw_cmd[0])*step_z+step_z)*(math.sin(angle_z)))
bge.render.drawLine([x2,y2, z2], [x3,y3,z3], draw_cmd[5])
draw_cmd[0] = draw_cmd[0]-scene.objects['Terrain']['speed']
# if scene.objects['Terrain']['speed']<1:
# draw_cmd[0] = draw_cmd[0]-scene.objects['Terrain']['speed']
# else:
# draw_cmd[0] = draw_cmd[0]-1
# bge.render.drawLine([draw_cmd[2][0]+((6-draw_cmd[0])*0.25)*(math.cos(draw_cmd[4])), draw_cmd[2][1]+((6-draw_cmd[0])*0.25)*(math.sin(draw_cmd[4])),draw_cmd[2][2]],
# [draw_cmd[2][0]+((6-draw_cmd[0])*0.25+0.25)*(math.cos(draw_cmd[4])), draw_cmd[2][1]+((6-draw_cmd[0])*0.25+0.25)*(math.sin(draw_cmd[4])),draw_cmd[2][2]],
# draw_cmd[5])
# Mage (cast)
# FIXME : Problème
if draw_cmd[1]=="cast": # Mage (cast)
circle(draw_cmd[2], 3.1-draw_cmd[0]*0.1, draw_cmd[3])
circle(draw_cmd[2], 3-draw_cmd[0]*0.1, draw_cmd[3])
circle(draw_cmd[2], 2.9-draw_cmd[0]*0.1, draw_cmd[3])
draw_cmd[0] = draw_cmd[0]-scene.objects['Terrain']['speed']
# if scene.objects['Terrain']['speed']<=2:
# draw_cmd[0] = draw_cmd[0]-scene.objects['Terrain']['speed']
# if scene.objects['Terrain']['speed']==4:
# draw_cmd[0] = draw_cmd[0]-draw_cmd[4]/2
# circle(draw_cmd[2], 3, draw_cmd[3]) # simple
# radius=[3,3,2.5,2.5,2,2,1.5,1.5,1,1,1] # basé sur un tableau
# circle(draw_cmd[2], radius[draw_cmd[0]], draw_cmd[3])
# Rayon
if draw_cmd[1]=="ray":
if draw_cmd[3] in scene.objects:
x0 = draw_cmd[2][0]+0.25*(math.cos(draw_cmd[4]))
y0 = draw_cmd[2][1]+0.25*(math.sin(draw_cmd[4]))
x1 = scene.objects[draw_cmd[3]].worldPosition.x
y1 = scene.objects[draw_cmd[3]].worldPosition.y
z1 = scene.objects[draw_cmd[3]].worldPosition.z
bge.render.drawLine([x0,y0, draw_cmd[2][2]], [x1,y1,z1], draw_cmd[5]) # suivi minion
# bge.render.drawLine([draw_cmd[2][0]+0.25*(math.cos(draw_cmd[4])), draw_cmd[2][1]+0.25*(math.sin(draw_cmd[4])), draw_cmd[2][2]], draw_cmd[3], draw_cmd[5]) # décalage minion
# bge.render.drawLine(draw_cmd[2], draw_cmd[3], draw_cmd[5]) # simple
draw_cmd[0] = draw_cmd[0]-scene.objects['Terrain']['speed']
# Suppression des draws finis
i=0
for draw_cmd in scene.objects['Terrain']['draw_list']:
if draw_cmd[0]<=0:
scene.objects['Terrain']['draw_list'].pop(i)
else:
i=i+1
if len(scene.objects['Terrain']['draw_list'])==0:
scene.objects['Terrain']['draw_process']=False