mirror of
https://forge.apps.education.fr/phroy/codetower.git
synced 2024-01-27 11:35:17 +01:00
gestion des casts (buff et debuff)
This commit is contained in:
parent
974b3abc21
commit
52e91a553b
4
IDEAS.md
4
IDEAS.md
@ -5,11 +5,11 @@
|
|||||||
* if
|
* if
|
||||||
* Manipulation de liste
|
* Manipulation de liste
|
||||||
* Manipulation de dictionnaire
|
* Manipulation de dictionnaire
|
||||||
* Création d'objet (méthode et propriété) - POO
|
* Création d'objet (méthodes et propriétés) - POO
|
||||||
|
|
||||||
### Principes
|
### Principes
|
||||||
* Une tour ou une techno coûte un niveau général (100 crédits, voire de plus en plus chère)
|
* Une tour ou une techno coûte un niveau général (100 crédits, voire de plus en plus chère)
|
||||||
* Les tours évolues (xp) individuellement niveau local et peuvent accèder à des nouvelles technos
|
* Les tours évolues (xp) individuellement niveau local et peuvent accèder à des nouvelles technos ou sorts
|
||||||
|
|
||||||
### Stratégies en place
|
### Stratégies en place
|
||||||
* Créer une tour : wave 1
|
* Créer une tour : wave 1
|
||||||
|
BIN
codetower-18.blend
Normal file
BIN
codetower-18.blend
Normal file
Binary file not shown.
48
ct.py
48
ct.py
@ -13,14 +13,14 @@ import xml.etree.ElementTree as ET # Creating/parsing XML file
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# ct.py
|
# ct.py
|
||||||
# @title: the CodeTower game
|
# @title: the CodeTower game
|
||||||
# @project: Blender-EduTech
|
# @project: CodeTower
|
||||||
# @lang: fr,en
|
# @lang: fr,en
|
||||||
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
||||||
# @copyright: Copyright (C) 2022 Philippe Roy
|
# @copyright: Copyright (C) 2022 Philippe Roy
|
||||||
# @license: GNU GPL
|
# @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.
|
# Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
|
||||||
# This game is a tower defense coding game. The towers are driven with Python code.
|
|
||||||
#
|
#
|
||||||
# Commands trigged by button : cmd_*
|
# Commands trigged by button : cmd_*
|
||||||
# Commands trigged by 3D scene objects : scn_*
|
# Commands trigged by 3D scene objects : scn_*
|
||||||
@ -32,7 +32,7 @@ import xml.etree.ElementTree as ET # Creating/parsing XML file
|
|||||||
# Dynamic import user file
|
# Dynamic import user file
|
||||||
sys.setrecursionlimit(10**5) # Limite sur la récursivité (valeur par défaut : 1000) -> segfault de Blender
|
sys.setrecursionlimit(10**5) # Limite sur la récursivité (valeur par défaut : 1000) -> segfault de Blender
|
||||||
importlib.invalidate_caches()
|
importlib.invalidate_caches()
|
||||||
ct_map = importlib.import_module('ct_map1') # waves script
|
ct_map = importlib.import_module('ct_map1') # waves script
|
||||||
ct_cmd = importlib.import_module('ct_cmd') # user script (commands)
|
ct_cmd = importlib.import_module('ct_cmd') # user script (commands)
|
||||||
|
|
||||||
# UPBGE scene
|
# UPBGE scene
|
||||||
@ -133,14 +133,14 @@ def points_maj (cont):
|
|||||||
scene.objects['Points']['tics'] +=1
|
scene.objects['Points']['tics'] +=1
|
||||||
|
|
||||||
# Augmentation d'un niveau
|
# Augmentation d'un niveau
|
||||||
if scene.objects['Points']['coins']>100:
|
if scene.objects['Points']['coins']>=100:
|
||||||
scene.objects['Points']['level_max'] +=1
|
scene.objects['Points']['level_max'] +=1
|
||||||
scene.objects['Points']['coins'] -=100
|
scene.objects['Points']['coins'] -=100
|
||||||
|
|
||||||
# Level trop élevé
|
# Level trop élevé
|
||||||
if scene.objects['Points']['level'] > scene.objects['Points']['level_max'] :
|
if scene.objects['Points']['level'] > scene.objects['Points']['level_max'] :
|
||||||
scene.objects['Level_text'].color = color_text_red
|
scene.objects['Level_text'].color = color_text_red
|
||||||
if scene.objects['Level_text'].color == color_text_red and scene.objects['Points']['level'] <= scene.objects['Points']['level_max']:
|
if scene.objects['Points']['level'] <= scene.objects['Points']['level_max']:
|
||||||
scene.objects['Level_text'].color = color_text
|
scene.objects['Level_text'].color = color_text
|
||||||
|
|
||||||
# Ramasse minions perdues
|
# Ramasse minions perdues
|
||||||
@ -155,16 +155,6 @@ def points_maj (cont):
|
|||||||
print ("Minion lost : x or y outside the map (x, y, z, x', y') : ", obj_i.name, obj_i.worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z, abs(obj_i.worldLinearVelocity.x), abs(obj_i.worldLinearVelocity.y))
|
print ("Minion lost : x or y outside the map (x, y, z, x', y') : ", obj_i.name, obj_i.worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z, abs(obj_i.worldLinearVelocity.x), abs(obj_i.worldLinearVelocity.y))
|
||||||
obj_i['dead']=True
|
obj_i['dead']=True
|
||||||
|
|
||||||
|
|
||||||
# scene.objects['Points']['minions_state'].append([obj_i.name, worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z])
|
|
||||||
# if scene.objects['Points']['tics']%240 ==0:
|
|
||||||
# for obj_i in scene.objects:
|
|
||||||
# if "type_minion" in obj_i.getPropertyNames() and "type_towerminion" not in obj_i.getPropertyNames():
|
|
||||||
# if obj_i.name
|
|
||||||
# print (obj_i.name, obj_i.worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z)
|
|
||||||
# scene.objects['Points']['minions_state'].append([obj_i.name, worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z])
|
|
||||||
|
|
||||||
|
|
||||||
# Fin de la vague
|
# Fin de la vague
|
||||||
if scene.objects['Terrain']['thread_wave']==False and scene.objects['Terrain']['map_run'] == True :
|
if scene.objects['Terrain']['thread_wave']==False and scene.objects['Terrain']['map_run'] == True :
|
||||||
if scene.objects['Points']['minions_run']==0 :
|
if scene.objects['Points']['minions_run']==0 :
|
||||||
@ -292,7 +282,7 @@ def terrain_run ():
|
|||||||
scene.objects['Points']['time_begin']=time.localtime()
|
scene.objects['Points']['time_begin']=time.localtime()
|
||||||
if scene.objects['Terrain']['debug_flag']==False: # Lecture dynamique du script python (risque de Segfault de Blender)
|
if scene.objects['Terrain']['debug_flag']==False: # Lecture dynamique du script python (risque de Segfault de Blender)
|
||||||
importlib.reload(ct_cmd)
|
importlib.reload(ct_cmd)
|
||||||
importlib.reload(ct_map)
|
# importlib.reload(ct_map)
|
||||||
ct_cmd.start() # Execution du script utilisateur
|
ct_cmd.start() # Execution du script utilisateur
|
||||||
ct_map.start(1) # Lancement du script de la permière vague
|
ct_map.start(1) # Lancement du script de la permière vague
|
||||||
|
|
||||||
@ -508,7 +498,7 @@ def cmd_hl(cont):
|
|||||||
def cmd_click (cont):
|
def cmd_click (cont):
|
||||||
obj = cont.owner
|
obj = cont.owner
|
||||||
if cont.sensors['Click'].status == JUST_ACTIVATED and cont.sensors['MO'].positive and scene.objects['Terrain']['manip_mode']==0:
|
if cont.sensors['Click'].status == JUST_ACTIVATED and cont.sensors['MO'].positive and scene.objects['Terrain']['manip_mode']==0:
|
||||||
if obj.name=="Pause" or obj.name=="Run":
|
if obj.name=="Pause" or obj.name=="Run": # FIXME problème sur l'icone pause
|
||||||
terrain_run ()
|
terrain_run ()
|
||||||
if obj.name=="Stop":
|
if obj.name=="Stop":
|
||||||
terrain_stop ()
|
terrain_stop ()
|
||||||
@ -645,28 +635,6 @@ def manip(cont):
|
|||||||
delta_x=cont.sensors['DownM'].position[0]-obj['click_x']
|
delta_x=cont.sensors['DownM'].position[0]-obj['click_x']
|
||||||
delta_y=cont.sensors['DownM'].position[1]-obj['click_y']
|
delta_y=cont.sensors['DownM'].position[1]-obj['click_y']
|
||||||
|
|
||||||
# Orbit (1280 * 720 px) Pas de Orbit ici
|
|
||||||
# if obj['manip_mode']==0:
|
|
||||||
# scene.objects['Orbit'].color=color_cmd
|
|
||||||
# scene.objects['Orbit'].setVisible(True,False)
|
|
||||||
# dist_orbit = math.sqrt(((1280/2)-obj['click_x'])**2+((720/2)-obj['click_y'])**2)
|
|
||||||
# if dist_orbit<235 : # Orbit sur x et z
|
|
||||||
# n=10
|
|
||||||
# pas_x=(delta_x*40*sensibilite_orbit)/n
|
|
||||||
# pas_y=(((1280/2)-cont.sensors['DownM'].position[0])+((720/2)-cont.sensors['DownM'].position[1]))*0.005
|
|
||||||
# pas_z=(delta_y*40*sensibilite_orbit)/n
|
|
||||||
# for i in range (n):
|
|
||||||
# bge.render.drawLine([scene.objects['Orbit'].worldPosition.x+pas_x*i, scene.objects['Orbit'].worldPosition.y+abs(pas_y*math.sin((3.14*i)/n)), scene.objects['Orbit'].worldPosition.z-pas_z*i],
|
|
||||||
# [scene.objects['Orbit'].worldPosition.x+pas_x*(i+1), scene.objects['Orbit'].worldPosition.y+abs(pas_y*math.sin((3.14*(i+1))/n)), scene.objects['Orbit'].worldPosition.z-pas_z*(i+1)],
|
|
||||||
# [0.8, 0.619, 0.021])
|
|
||||||
# scene.objects['Terrain'].applyRotation((delta_y*sensibilite_orbit, 0, delta_x*sensibilite_orbit), True)
|
|
||||||
# else: # Orbit sur y
|
|
||||||
# scene.objects['Orbit'].color=color_cmd_hl
|
|
||||||
# if abs(delta_x) >= abs(delta_y):
|
|
||||||
# scene.objects['Terrain'].applyRotation((0, delta_x*sensibilite_orbit, 0), True)
|
|
||||||
# else:
|
|
||||||
# scene.objects['Terrain'].applyRotation((0, delta_y*sensibilite_orbit, 0), True)
|
|
||||||
|
|
||||||
# Pan
|
# Pan
|
||||||
if obj['manip_mode']==1: # Shift
|
if obj['manip_mode']==1: # Shift
|
||||||
scene.objects['Camera'].applyMovement((delta_x*-sensibilite_pan, delta_y*sensibilite_pan, 0), True)
|
scene.objects['Camera'].applyMovement((delta_x*-sensibilite_pan, delta_y*sensibilite_pan, 0), True)
|
||||||
|
@ -9,8 +9,8 @@ from ct_lib import * # Bibliothèque CodeTower
|
|||||||
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
||||||
# @copyright: Copyright (C) 2022 Philippe Roy
|
# @copyright: Copyright (C) 2022 Philippe Roy
|
||||||
# @license: GNU GPL
|
# @license: GNU GPL
|
||||||
#
|
#
|
||||||
# En: This game is a tower defense coding game. The towers are driven with Python code.
|
# En: This game is a tower defense coding game. The towers are driven by Python code.
|
||||||
# Fr: Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
|
# Fr: Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
|
||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@ -79,11 +79,11 @@ def commands():
|
|||||||
if ct_level()==1:
|
if ct_level()==1:
|
||||||
ct_build(4,5, "Archer tower", "Tower #1", blue, "square-A")
|
ct_build(4,5, "Archer tower", "Tower #1", blue, "square-A")
|
||||||
|
|
||||||
if ct_level()==2:
|
if ct_level()==1:
|
||||||
ct_build(4,-2, "Archer tower", "Tower #2", blue, "square-A")
|
ct_build(4,-2, "Archer tower", "Tower #2", blue, "square-A")
|
||||||
|
|
||||||
if ct_level()==3:
|
if ct_level()==3:
|
||||||
ct_build(5,6, "Mage tower", "Tower #3", yellow, "round-A")
|
ct_build(5,5, "Mage tower", "Tower #3", magenta, "square-B")
|
||||||
|
|
||||||
|
|
||||||
end() # End of cycle << DONT CHANGE THIS LINE >>
|
end() # End of cycle << DONT CHANGE THIS LINE >>
|
||||||
|
@ -10,8 +10,8 @@ from collections import OrderedDict
|
|||||||
# @copyright: Copyright (C) 2022 Philippe Roy
|
# @copyright: Copyright (C) 2022 Philippe Roy
|
||||||
# @license: GNU GPL
|
# @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.
|
# Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
|
||||||
# This game is a tower defense coding game. The towers are driven with Python code.
|
|
||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
69
ct_lib.py
69
ct_lib.py
@ -17,8 +17,8 @@ import random
|
|||||||
# @copyright: Copyright (C) 2022 Philippe Roy
|
# @copyright: Copyright (C) 2022 Philippe Roy
|
||||||
# @license: GNU GPL
|
# @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.
|
# Ce simulateur est un jeu du type tower defense où les tours sont à piloter par la programmation Python.
|
||||||
# This game is a tower defense coding game. The towers are driven with Python code.
|
|
||||||
#
|
#
|
||||||
# Commands trigged by button : cmd_*
|
# Commands trigged by button : cmd_*
|
||||||
# Commands trigged by 3D scene objects : scn_*
|
# Commands trigged by 3D scene objects : scn_*
|
||||||
@ -133,7 +133,6 @@ def thread_stop(threads, type_txt):
|
|||||||
print ("There are zombies threads",type_txt, ".")
|
print ("There are zombies threads",type_txt, ".")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Vagues (minions)
|
# Vagues (minions)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@ -176,7 +175,7 @@ def ct_minion_details(x,y,cat,level,body="Knight_m_A_common"):
|
|||||||
minion['bounty']=minion_carac[category][5]
|
minion['bounty']=minion_carac[category][5]
|
||||||
minion['lifes_damage']=minion_carac[category][6]
|
minion['lifes_damage']=minion_carac[category][6]
|
||||||
minion['buff']=[]
|
minion['buff']=[]
|
||||||
minion['debuff']=[]
|
minion['resist']=[]
|
||||||
|
|
||||||
# Actuator Steering
|
# Actuator Steering
|
||||||
minion.actuators['Steering'].navmesh=scene.objects[scene.objects['Terrain']['navmesh']]
|
minion.actuators['Steering'].navmesh=scene.objects[scene.objects['Terrain']['navmesh']]
|
||||||
@ -184,16 +183,6 @@ def ct_minion_details(x,y,cat,level,body="Knight_m_A_common"):
|
|||||||
minion.actuators['Steering'].distance=0.5
|
minion.actuators['Steering'].distance=0.5
|
||||||
minion.actuators['Steering'].velocity=minion['speed_base']*scene.objects['Terrain']['speed']
|
minion.actuators['Steering'].velocity=minion['speed_base']*scene.objects['Terrain']['speed']
|
||||||
|
|
||||||
def scn_minion_lost(cont):
|
|
||||||
obj = cont.owner
|
|
||||||
print (obj.name, obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z)
|
|
||||||
# for obj_i in scene.objects:
|
|
||||||
# if "type_minion" in obj_i.getPropertyNames() and "type_towerminion" not in obj_i.getPropertyNames():
|
|
||||||
# if obj_i.worldPosition.y>11:
|
|
||||||
# obj_i.endObject()
|
|
||||||
# # print (obj_i.name, obj_i.worldPosition.x, obj_i.worldPosition.y, obj_i.worldPosition.z)
|
|
||||||
|
|
||||||
|
|
||||||
# Destruction d'un minion
|
# Destruction d'un minion
|
||||||
def scn_minion_dead(cont):
|
def scn_minion_dead(cont):
|
||||||
obj = cont.owner
|
obj = cont.owner
|
||||||
@ -203,6 +192,31 @@ def scn_minion_dead(cont):
|
|||||||
scene.objects['Points']['coins']= scene.objects['Points']['coins']+obj['bounty']
|
scene.objects['Points']['coins']= scene.objects['Points']['coins']+obj['bounty']
|
||||||
obj.endObject()
|
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
|
# Tours
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@ -261,20 +275,12 @@ def ct_build_details(x,y, cat='Archer tower', tower_name="Tower", color=tower_pu
|
|||||||
tour['speed']=tower_carac[cat][2]
|
tour['speed']=tower_carac[cat][2]
|
||||||
tour['range']=tower_carac[cat][3]
|
tour['range']=tower_carac[cat][3]
|
||||||
tour['techno']=[]
|
tour['techno']=[]
|
||||||
tour['cast']=""
|
tour['cast']="slow"
|
||||||
|
tour['cast_duration']=2
|
||||||
# Gestion du draw
|
|
||||||
tour['fire_frame']=0
|
|
||||||
tour['fire_target']=[]
|
|
||||||
|
|
||||||
# Capteur Near
|
# Capteur Near
|
||||||
tour.sensors['Near'].distance=tour['range']
|
tour.sensors['Near'].distance=tour['range']
|
||||||
tour.sensors['Near'].skippedTicks =round(1/(tour['speed']*scene.objects['Terrain']['speed']))
|
tour.sensors['Near'].skippedTicks =round(1/(tour['speed']*scene.objects['Terrain']['speed']))
|
||||||
|
|
||||||
# for obj_i in scene.objects:
|
|
||||||
# if "type_tower" in obj_i.getPropertyNames():
|
|
||||||
# print (obj_i['tower_name'])
|
|
||||||
# print (tour.getPropertyNames())
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Suppression d'une tour
|
# Suppression d'une tour
|
||||||
@ -350,13 +356,10 @@ def scn_tower_near(cont):
|
|||||||
if target['hp']<=0:
|
if target['hp']<=0:
|
||||||
target['dead']=True
|
target['dead']=True
|
||||||
|
|
||||||
# Buf
|
# Cast (buff and debuff)
|
||||||
if obj['cat']=="Mage tower":
|
if obj['cat']=="Mage tower":
|
||||||
i =0
|
|
||||||
for target_i in sensor.hitObjectList:
|
for target_i in sensor.hitObjectList:
|
||||||
if target_i.actuators['Steering'].velocity== target['speed_base']*scene.objects['Terrain']['speed']:
|
target_i['buff'].append([obj['cast'], obj['cast_duration']])
|
||||||
target_i.actuators['Steering'].velocity /= 2
|
|
||||||
i+=1
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Carte
|
# Carte
|
||||||
@ -464,16 +467,6 @@ def circle (center, radius, color):
|
|||||||
bge.render.drawLine([x0,y0,center[2]],[x1,y1,center[2]],color)
|
bge.render.drawLine([x0,y0,center[2]],[x1,y1,center[2]],color)
|
||||||
ang += ang_step
|
ang += ang_step
|
||||||
|
|
||||||
# Fire : projectile ou cast
|
|
||||||
# def scn_tower_fire(cont):
|
|
||||||
# obj = cont.owner
|
|
||||||
# if obj["fire_frame"]!=0:
|
|
||||||
# if obj['cat']=="Archer tower": # Archer (tir de flêche)
|
|
||||||
# bge.render.drawLine([obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8],obj['fire_target'][0], ray_yellow)
|
|
||||||
# if obj['cat']=="Mage tower": # Mage (cast)
|
|
||||||
# circle([obj.worldPosition.x, obj.worldPosition.y, obj.worldPosition.z+0.8], 3, ray_blue)
|
|
||||||
# obj["fire_frame"] -=1
|
|
||||||
|
|
||||||
# Affiche les draws en cours
|
# Affiche les draws en cours
|
||||||
# FIXME: tir sur le plus avancé
|
# FIXME: tir sur le plus avancé
|
||||||
#
|
#
|
||||||
|
@ -9,10 +9,9 @@ from ct_lib import * # Bibliothèque CodeTower
|
|||||||
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
# @authors: Philippe Roy <philippe.roy@ac-grenoble.fr>
|
||||||
# @copyright: Copyright (C) 2022 Philippe Roy
|
# @copyright: Copyright (C) 2022 Philippe Roy
|
||||||
# @license: GNU GPL
|
# @license: GNU GPL
|
||||||
#
|
|
||||||
# This game is a tower defense coding game. The towers are driven with Python code.
|
|
||||||
#
|
#
|
||||||
# The file is the the map and waves definition
|
# This game is a tower defense coding game. The towers are driven by Python code.
|
||||||
|
# The file is the the map and waves definition.
|
||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user