Merge branch 'exp' of https://git.nfedorov.dev/DragonSpirit/KickSurvivors into exp
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter="*jpeg"
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="../Kick/KickSurvivors.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
binary_format/embed_pck=false
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon=""
|
||||||
|
application/console_wrapper_icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name=""
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright=""
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
@@ -120,6 +120,22 @@ func setup(type: String, wave: int) -> void:
|
|||||||
kick_tier = enemy_level
|
kick_tier = enemy_level
|
||||||
toughness_tier = enemy_level
|
toughness_tier = enemy_level
|
||||||
_update_label()
|
_update_label()
|
||||||
|
_apply_mesh(enemy_level)
|
||||||
|
|
||||||
|
func _apply_mesh(level: int) -> void:
|
||||||
|
var idx := clampi(level, 1, 3)
|
||||||
|
var mesh_res := load("res://assets/gnome%d.obj" % idx) as Mesh
|
||||||
|
if mesh_res == null:
|
||||||
|
return
|
||||||
|
var new_mat := StandardMaterial3D.new()
|
||||||
|
var tex := load("res://assets/gnome%d.png" % idx) as Texture2D
|
||||||
|
if tex != null:
|
||||||
|
new_mat.albedo_texture = tex
|
||||||
|
new_mat.albedo_color = Color.WHITE
|
||||||
|
mesh_node.mesh = mesh_res
|
||||||
|
mesh_node.material_override = new_mat
|
||||||
|
mat = new_mat
|
||||||
|
COLOR_CHASE = Color.WHITE
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
match state:
|
match state:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class_name GameSettings
|
||||||
|
|
||||||
|
## "immortal" — no damage, tier-based kick force
|
||||||
|
## "survival" — takes damage, fixed kick force
|
||||||
|
static var difficulty: String = "immortal"
|
||||||
|
|
||||||
|
## enemy spawn interval in seconds
|
||||||
|
static var enemy_spawn_interval: float = 10.0
|
||||||
|
|
||||||
|
## item (rock/stick) respawn delay in seconds
|
||||||
|
static var item_respawn_delay: float = 20.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://deu62njwuhjd3
|
||||||
@@ -52,6 +52,11 @@ static func resolve(owner: Node3D, other: Node3D, owner_vel: Vector3) -> bool:
|
|||||||
static func _execute_recipe(a: Node3D, b: Node3D, recipe: Dictionary) -> void:
|
static func _execute_recipe(a: Node3D, b: Node3D, recipe: Dictionary) -> void:
|
||||||
var pos := (a.global_position + b.global_position) * 0.5
|
var pos := (a.global_position + b.global_position) * 0.5
|
||||||
var parent := a.get_parent()
|
var parent := a.get_parent()
|
||||||
|
# Emit before freeing so respawn counters in Main decrement correctly.
|
||||||
|
if a.has_signal("destroyed"):
|
||||||
|
a.emit_signal("destroyed")
|
||||||
|
if b.has_signal("destroyed"):
|
||||||
|
b.emit_signal("destroyed")
|
||||||
a.queue_free()
|
a.queue_free()
|
||||||
b.queue_free()
|
b.queue_free()
|
||||||
var scene: PackedScene = load(recipe["result_scene"])
|
var scene: PackedScene = load(recipe["result_scene"])
|
||||||
|
|||||||
+6
-6
@@ -177,7 +177,8 @@ func _on_stick_destroyed() -> void:
|
|||||||
return
|
return
|
||||||
if sticks_on_field + sticks_pending < STICK_LIMIT:
|
if sticks_on_field + sticks_pending < STICK_LIMIT:
|
||||||
sticks_pending += 1
|
sticks_pending += 1
|
||||||
await get_tree().create_timer(10.0).timeout
|
await get_tree().create_timer(GameSettings.item_respawn_delay).timeout
|
||||||
|
|
||||||
sticks_pending -= 1
|
sticks_pending -= 1
|
||||||
if game_active:
|
if game_active:
|
||||||
_spawn_single_stick()
|
_spawn_single_stick()
|
||||||
@@ -213,7 +214,7 @@ func _on_rock_destroyed() -> void:
|
|||||||
return
|
return
|
||||||
if rocks_on_field + rocks_pending < _get_rock_limit():
|
if rocks_on_field + rocks_pending < _get_rock_limit():
|
||||||
rocks_pending += 1
|
rocks_pending += 1
|
||||||
await get_tree().create_timer(10.0).timeout
|
await get_tree().create_timer(GameSettings.item_respawn_delay).timeout
|
||||||
rocks_pending -= 1
|
rocks_pending -= 1
|
||||||
if game_active:
|
if game_active:
|
||||||
_spawn_single_rock()
|
_spawn_single_rock()
|
||||||
@@ -244,7 +245,7 @@ func _start_game() -> void:
|
|||||||
Enemy.first_iron_spawned = false
|
Enemy.first_iron_spawned = false
|
||||||
Enemy.first_essence_spawned = false
|
Enemy.first_essence_spawned = false
|
||||||
_update_labels()
|
_update_labels()
|
||||||
spawn_timer.wait_time = 1.4
|
spawn_timer.wait_time = GameSettings.enemy_spawn_interval
|
||||||
spawn_timer.connect("timeout", _on_spawn_timer)
|
spawn_timer.connect("timeout", _on_spawn_timer)
|
||||||
spawn_timer.start()
|
spawn_timer.start()
|
||||||
|
|
||||||
@@ -252,7 +253,7 @@ func _on_spawn_timer() -> void:
|
|||||||
if not game_active or upgrading:
|
if not game_active or upgrading:
|
||||||
return
|
return
|
||||||
_spawn_enemy()
|
_spawn_enemy()
|
||||||
spawn_timer.wait_time = SPAWN_TIME # max(0.25, 1.4 - wave * 0.07)
|
spawn_timer.wait_time = GameSettings.enemy_spawn_interval
|
||||||
|
|
||||||
func _spawn_enemy() -> void:
|
func _spawn_enemy() -> void:
|
||||||
var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D
|
var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D
|
||||||
@@ -306,6 +307,7 @@ func _spawn_upgraded_enemy(pos: Vector3, type: String, level: int, w: int) -> Ch
|
|||||||
enemy.kick_tier = level
|
enemy.kick_tier = level
|
||||||
enemy.toughness_tier = level
|
enemy.toughness_tier = level
|
||||||
enemy.call("_update_label")
|
enemy.call("_update_label")
|
||||||
|
enemy.call("_apply_mesh", level)
|
||||||
enemy.global_position = pos
|
enemy.global_position = pos
|
||||||
enemy.connect("died", _on_enemy_died)
|
enemy.connect("died", _on_enemy_died)
|
||||||
enemy.connect("merged", _on_enemy_merged)
|
enemy.connect("merged", _on_enemy_merged)
|
||||||
@@ -318,8 +320,6 @@ func _spawn_upgraded_enemy(pos: Vector3, type: String, level: int, w: int) -> Ch
|
|||||||
var old_size: Vector3 = s3d.size
|
var old_size: Vector3 = s3d.size
|
||||||
col_shape.shape = BoxShape3D.new()
|
col_shape.shape = BoxShape3D.new()
|
||||||
(col_shape.shape as BoxShape3D).size = old_size * s
|
(col_shape.shape as BoxShape3D).size = old_size * s
|
||||||
var color := Color(1.0, 1.0, 0.5) if level > 2 else Color(1.0, 0.9, 0.3)
|
|
||||||
tw.tween_property(enemy.mat, "albedo_color", color, 0.25)
|
|
||||||
if level >= 3 and not first_boss_spawned:
|
if level >= 3 and not first_boss_spawned:
|
||||||
first_boss_spawned = true
|
first_boss_spawned = true
|
||||||
_start_boss_phase()
|
_start_boss_phase()
|
||||||
|
|||||||
+130
-2
@@ -3,6 +3,7 @@ extends Control
|
|||||||
static var volume: float = 100.0
|
static var volume: float = 100.0
|
||||||
|
|
||||||
var settings_panel: Panel
|
var settings_panel: Panel
|
||||||
|
var difficulty_panel: Panel
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
@@ -46,17 +47,63 @@ func _build_ui() -> void:
|
|||||||
vbox.add_child(_btn("Выход", _on_exit))
|
vbox.add_child(_btn("Выход", _on_exit))
|
||||||
|
|
||||||
_build_settings_panel()
|
_build_settings_panel()
|
||||||
|
_build_difficulty_panel()
|
||||||
|
|
||||||
|
func _big_btn(text: String, width: float, cb: Callable) -> Button:
|
||||||
|
var b := _btn(text, cb)
|
||||||
|
b.custom_minimum_size = Vector2(width, 72)
|
||||||
|
b.add_theme_font_size_override("font_size", 18)
|
||||||
|
return b
|
||||||
|
|
||||||
func _btn(text: String, cb: Callable) -> Button:
|
func _btn(text: String, cb: Callable) -> Button:
|
||||||
var b := Button.new()
|
var b := Button.new()
|
||||||
b.text = text
|
b.text = text
|
||||||
b.custom_minimum_size = Vector2(220, 54)
|
b.custom_minimum_size = Vector2(220, 54)
|
||||||
b.add_theme_font_size_override("font_size", 20)
|
b.add_theme_font_size_override("font_size", 20)
|
||||||
|
for state in ["normal", "hover", "pressed", "focus", "disabled"]:
|
||||||
|
var sb := StyleBoxFlat.new()
|
||||||
|
sb.bg_color = Color(0.18, 0.13, 0.30) if state == "normal" else (
|
||||||
|
Color(0.28, 0.20, 0.46) if state == "hover" else
|
||||||
|
Color(0.12, 0.09, 0.22))
|
||||||
|
sb.border_width_left = 1
|
||||||
|
sb.border_width_right = 1
|
||||||
|
sb.border_width_top = 1
|
||||||
|
sb.border_width_bottom = 1
|
||||||
|
sb.border_color = Color(0.50, 0.38, 0.75)
|
||||||
|
sb.corner_radius_top_left = 6
|
||||||
|
sb.corner_radius_top_right = 6
|
||||||
|
sb.corner_radius_bottom_left = 6
|
||||||
|
sb.corner_radius_bottom_right = 6
|
||||||
|
sb.content_margin_left = 20
|
||||||
|
sb.content_margin_right = 20
|
||||||
|
sb.content_margin_top = 12
|
||||||
|
sb.content_margin_bottom = 12
|
||||||
|
b.add_theme_stylebox_override(state, sb)
|
||||||
b.connect("pressed", cb)
|
b.connect("pressed", cb)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
static func _make_opaque_panel() -> Panel:
|
||||||
|
var p := Panel.new()
|
||||||
|
var sb := StyleBoxFlat.new()
|
||||||
|
sb.bg_color = Color(0.08, 0.06, 0.14)
|
||||||
|
sb.border_width_left = 2
|
||||||
|
sb.border_width_right = 2
|
||||||
|
sb.border_width_top = 2
|
||||||
|
sb.border_width_bottom = 2
|
||||||
|
sb.border_color = Color(0.35, 0.28, 0.55)
|
||||||
|
sb.corner_radius_top_left = 8
|
||||||
|
sb.corner_radius_top_right = 8
|
||||||
|
sb.corner_radius_bottom_left = 8
|
||||||
|
sb.corner_radius_bottom_right = 8
|
||||||
|
sb.content_margin_left = 24
|
||||||
|
sb.content_margin_right = 24
|
||||||
|
sb.content_margin_top = 20
|
||||||
|
sb.content_margin_bottom = 20
|
||||||
|
p.add_theme_stylebox_override("panel", sb)
|
||||||
|
return p
|
||||||
|
|
||||||
func _build_settings_panel() -> void:
|
func _build_settings_panel() -> void:
|
||||||
settings_panel = Panel.new()
|
settings_panel = _make_opaque_panel()
|
||||||
settings_panel.visible = false
|
settings_panel.visible = false
|
||||||
settings_panel.anchor_left = 0.5
|
settings_panel.anchor_left = 0.5
|
||||||
settings_panel.anchor_right = 0.5
|
settings_panel.anchor_right = 0.5
|
||||||
@@ -105,8 +152,89 @@ func _apply_volume(v: float) -> void:
|
|||||||
var db := linear_to_db(v / 100.0) if v > 0.0 else -80.0
|
var db := linear_to_db(v / 100.0) if v > 0.0 else -80.0
|
||||||
AudioServer.set_bus_volume_db(0, db)
|
AudioServer.set_bus_volume_db(0, db)
|
||||||
|
|
||||||
|
func _build_difficulty_panel() -> void:
|
||||||
|
difficulty_panel = _make_opaque_panel()
|
||||||
|
difficulty_panel.visible = false
|
||||||
|
difficulty_panel.anchor_left = 0.5
|
||||||
|
difficulty_panel.anchor_right = 0.5
|
||||||
|
difficulty_panel.anchor_top = 0.5
|
||||||
|
difficulty_panel.anchor_bottom = 0.5
|
||||||
|
difficulty_panel.offset_left = -310
|
||||||
|
difficulty_panel.offset_right = 310
|
||||||
|
difficulty_panel.offset_top = -240
|
||||||
|
difficulty_panel.offset_bottom = 240
|
||||||
|
add_child(difficulty_panel)
|
||||||
|
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
margin.add_theme_constant_override("margin_left", 24)
|
||||||
|
margin.add_theme_constant_override("margin_right", 24)
|
||||||
|
margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 20)
|
||||||
|
difficulty_panel.add_child(margin)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
vbox.add_theme_constant_override("separation", 16)
|
||||||
|
margin.add_child(vbox)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Выберите сложность"
|
||||||
|
title.add_theme_font_size_override("font_size", 26)
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vbox.add_child(title)
|
||||||
|
|
||||||
|
vbox.add_child(_big_btn("Бессмертный\nНет урона · тир пинков от мелких врагов", 540, func() -> void:
|
||||||
|
GameSettings.difficulty = "immortal"
|
||||||
|
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
||||||
|
))
|
||||||
|
|
||||||
|
vbox.add_child(_big_btn("Выживание\nПолучаешь урон · фиксированная сила пинка", 540, func() -> void:
|
||||||
|
GameSettings.difficulty = "survival"
|
||||||
|
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
||||||
|
))
|
||||||
|
|
||||||
|
# Spawn speed row
|
||||||
|
var spawn_lbl := Label.new()
|
||||||
|
spawn_lbl.text = "Скорость спавна:"
|
||||||
|
spawn_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
spawn_lbl.add_theme_font_size_override("font_size", 16)
|
||||||
|
spawn_lbl.add_theme_color_override("font_color", Color(0.85, 0.78, 1.0))
|
||||||
|
vbox.add_child(spawn_lbl)
|
||||||
|
|
||||||
|
var hbox := HBoxContainer.new()
|
||||||
|
hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
hbox.add_theme_constant_override("separation", 10)
|
||||||
|
vbox.add_child(hbox)
|
||||||
|
|
||||||
|
var spawn_options := [
|
||||||
|
["Медленно", 15.0, 35.0],
|
||||||
|
["Нормально", 10.0, 20.0],
|
||||||
|
["Быстро", 5.0, 8.0],
|
||||||
|
]
|
||||||
|
var spawn_btns: Array[Button] = []
|
||||||
|
for opt in spawn_options:
|
||||||
|
var sb_btn := _btn(opt[0] as String, Callable())
|
||||||
|
sb_btn.custom_minimum_size = Vector2(110, 40)
|
||||||
|
sb_btn.add_theme_font_size_override("font_size", 15)
|
||||||
|
var enemy_iv: float = opt[1]
|
||||||
|
var item_iv: float = opt[2]
|
||||||
|
sb_btn.connect("pressed", func() -> void:
|
||||||
|
GameSettings.enemy_spawn_interval = enemy_iv
|
||||||
|
GameSettings.item_respawn_delay = item_iv
|
||||||
|
for b in spawn_btns:
|
||||||
|
b.modulate = Color(1, 1, 1)
|
||||||
|
sb_btn.modulate = Color(0.7, 1.0, 0.5)
|
||||||
|
)
|
||||||
|
hbox.add_child(sb_btn)
|
||||||
|
spawn_btns.append(sb_btn)
|
||||||
|
# default: normal
|
||||||
|
spawn_btns[1].modulate = Color(0.7, 1.0, 0.5)
|
||||||
|
|
||||||
|
vbox.add_child(_btn("Назад", func(): difficulty_panel.visible = false))
|
||||||
|
|
||||||
func _on_play() -> void:
|
func _on_play() -> void:
|
||||||
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
difficulty_panel.visible = true
|
||||||
|
|
||||||
func _on_settings() -> void:
|
func _on_settings() -> void:
|
||||||
settings_panel.visible = true
|
settings_panel.visible = true
|
||||||
|
|||||||
+6
-2
@@ -85,7 +85,7 @@ func _input(event: InputEvent) -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if not is_alive:
|
if not is_alive:
|
||||||
return
|
return
|
||||||
is_shielding = shield_tier > 0 and Input.is_key_pressed(KEY_SHIFT)
|
is_shielding = shield_tier > 0 and Input.is_key_pressed(KEY_SPACE)
|
||||||
_handle_movement(delta)
|
_handle_movement(delta)
|
||||||
_handle_kick(delta)
|
_handle_kick(delta)
|
||||||
_handle_iframes(delta)
|
_handle_iframes(delta)
|
||||||
@@ -180,7 +180,9 @@ func _do_kick() -> void:
|
|||||||
var obj_toughness: int = best.get("toughness_tier") if best.get("toughness_tier") != null else 0
|
var obj_toughness: int = best.get("toughness_tier") if best.get("toughness_tier") != null else 0
|
||||||
var diff_tier := kick_tier - obj_toughness
|
var diff_tier := kick_tier - obj_toughness
|
||||||
var force: float
|
var force: float
|
||||||
if diff_tier < 0:
|
if GameSettings.difficulty == "survival":
|
||||||
|
force = 50.0
|
||||||
|
elif diff_tier < 0:
|
||||||
force = 15.0
|
force = 15.0
|
||||||
elif diff_tier == 0:
|
elif diff_tier == 0:
|
||||||
force = 50.0
|
force = 50.0
|
||||||
@@ -229,6 +231,8 @@ func receive_kick(direction: Vector3, force: float) -> void:
|
|||||||
_squish_effect()
|
_squish_effect()
|
||||||
|
|
||||||
func take_damage(amount: int, attacker_toughness: int = 0) -> void:
|
func take_damage(amount: int, attacker_toughness: int = 0) -> void:
|
||||||
|
if GameSettings.difficulty == "immortal":
|
||||||
|
return
|
||||||
if not is_alive or invincible_timer > 0.0:
|
if not is_alive or invincible_timer > 0.0:
|
||||||
return
|
return
|
||||||
invincible_timer = IFRAMES_DURATION
|
invincible_timer = IFRAMES_DURATION
|
||||||
|
|||||||
Reference in New Issue
Block a user