diff --git a/gui/buy_menu/shop.gd b/gui/buy_menu/shop.gd index ea41fe6..66dfbcc 100644 --- a/gui/buy_menu/shop.gd +++ b/gui/buy_menu/shop.gd @@ -2,7 +2,7 @@ extends Node func can_buy(player_id: int,weapon: WeaponResource) -> bool: - return Session.player_data[player_id]["money"] >= weapon.cost + return Session.player_data[player_id]["money"] >= weapon.cost or LobbySettings.infinite_money func buy(player_id: int, weapon: WeaponResource) -> void: if not multiplayer.is_server() or can_buy(player_id,weapon) == false: @@ -27,6 +27,7 @@ func buy(player_id: int, weapon: WeaponResource) -> void: var player_data = Session.player_data[player_id] - player_data["money"] -= weapon.cost + if not LobbySettings.infinite_money: + player_data["money"] -= weapon.cost weapon_system.add(weapon.weapon_system_scene.instantiate(),slot) diff --git a/gui/host_menu/host_menu.gd b/gui/host_menu/host_menu.gd index e93b53b..a48c539 100644 --- a/gui/host_menu/host_menu.gd +++ b/gui/host_menu/host_menu.gd @@ -10,6 +10,7 @@ func _ready() -> void: %RoundBox.set_value_no_signal(LobbySettings.win_score) %AllowMultipleAbilityButton.set_pressed_no_signal(LobbySettings.allow_multiple_abilities) %AllowTeamSwitch.set_pressed_no_signal(LobbySettings.allow_team_switch) + %InfiniteMoney.set_pressed_no_signal(LobbySettings.infinite_money) var popup: PopupMenu = %MapsButton.get_popup() %MapsButton.text = LobbySettings.selected_map @@ -53,3 +54,7 @@ func _on_allow_team_switch_toggled(toggled_on: bool) -> void: func on_maps_index_pressed(index: int): %MapsButton.text = levels_by_index[index] LobbySettings.selected_map = levels_by_index[index] + + +func _on_infinite_money_toggled(toggled_on: bool) -> void: + LobbySettings.infinite_money = toggled_on diff --git a/gui/host_menu/host_menu.tscn b/gui/host_menu/host_menu.tscn index 2078870..e912b27 100644 --- a/gui/host_menu/host_menu.tscn +++ b/gui/host_menu/host_menu.tscn @@ -112,14 +112,24 @@ layout_mode = 2 folded = true title = "Геймплей" -[node name="AllowMultipleAbilityButton" type="CheckButton" parent="Gameplay"] -unique_name_in_owner = true +[node name="VBoxContainer" type="VBoxContainer" parent="Gameplay"] visible = false +layout_mode = 2 + +[node name="AllowMultipleAbilityButton" type="CheckButton" parent="Gameplay/VBoxContainer"] +unique_name_in_owner = true custom_minimum_size = Vector2(200, 0) layout_mode = 2 text = "Неограниченная закупка абилок" autowrap_mode = 2 +[node name="InfiniteMoney" type="CheckButton" parent="Gameplay/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Бесконечные деньги" +autowrap_mode = 2 + [node name="PortForward" type="FoldableContainer" parent="."] layout_mode = 2 folded = true @@ -166,7 +176,8 @@ text = "Перемешать" [connection signal="value_changed" from="Time/VBoxContainer/BuyTime/BuyTimeBox" to="." method="_on_buy_time_box_value_changed"] [connection signal="value_changed" from="Round/VBoxContainer/RoundAmount/RoundBox" to="." method="_on_round_box_value_changed"] [connection signal="value_changed" from="Round/VBoxContainer/TeamSwitchAmount/TeamSwitchBox" to="." method="_on_team_switch_box_value_changed"] -[connection signal="toggled" from="Gameplay/AllowMultipleAbilityButton" to="." method="_on_allow_multiple_ability_button_toggled"] +[connection signal="toggled" from="Gameplay/VBoxContainer/AllowMultipleAbilityButton" to="." method="_on_allow_multiple_ability_button_toggled"] +[connection signal="toggled" from="Gameplay/VBoxContainer/InfiniteMoney" to="." method="_on_infinite_money_toggled"] [connection signal="update_ip" from="PortForward/PortForwardContainer/ForwardPortButton" to="PortForward/PortForwardContainer/PublicIP" method="on_update_ip"] [connection signal="toggled" from="Teams/VBoxContainer/AllowTeamSwitch" to="." method="_on_allow_team_switch_toggled"] [connection signal="pressed" from="Teams/VBoxContainer/Shuffle" to="." method="_on_shuffle_pressed"] diff --git a/levels/prototype.tscn b/levels/prototype.tscn index 6354cc1..d42f582 100644 --- a/levels/prototype.tscn +++ b/levels/prototype.tscn @@ -298,7 +298,7 @@ script = ExtResource("11_02ic3") exlusion_list = [NodePath("MultiplayerSpawner"), NodePath("Bomb"), NodePath("Parenter")] [node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="DynamicObjectsContainer"] -_spawnable_scenes = PackedStringArray("uid://cxdgk74ln5xpn", "uid://dtbpyfdawb02b", "uid://dgfqppi21c2u0", "uid://b6qahd6q60js7", "uid://l4t1mflutm3t", "uid://b3xux7url8d2s") +_spawnable_scenes = PackedStringArray("uid://cxdgk74ln5xpn", "uid://dtbpyfdawb02b", "uid://dgfqppi21c2u0", "uid://b6qahd6q60js7", "uid://l4t1mflutm3t", "uid://b3xux7url8d2s", "uid://u8aj6fs32ql6") spawn_path = NodePath("..") [node name="Parenter" type="Node" parent="DynamicObjectsContainer"] diff --git a/multiplayer/lobby_settings.gd b/multiplayer/lobby_settings.gd index b5e0d26..7639939 100644 --- a/multiplayer/lobby_settings.gd +++ b/multiplayer/lobby_settings.gd @@ -9,4 +9,5 @@ var buy_time: float = 15.0 var round_time: float = 150.0 var allow_multiple_abilities: bool = false var allow_team_switch: bool = true +var infinite_money: bool = false var selected_map: StringName = "prototype" diff --git a/multiplayer/session.gd b/multiplayer/session.gd index 7e4ef1a..0cc5652 100644 --- a/multiplayer/session.gd +++ b/multiplayer/session.gd @@ -1,5 +1,7 @@ extends Node +const BULLET_HOLE: PackedScene = preload("uid://u8aj6fs32ql6") + enum TEAMS { DEFENCE, ATTACK, @@ -325,56 +327,8 @@ func shoot(id:int , limb_damage: int, torso_damage: int,head_damage: int, distan var collision = space.intersect_ray(ray) - if collision != {} and collision["collider"] is Player: - var hit_player: Player = collision["collider"] - var shape_object: CollisionShape3D = hit_player.shape_owner_get_owner(collision["shape"]) - var reduction: float = 1 - var damage: int = 0 - - match shape_object.get_groups()[0]: - "Head": - damage = head_damage - "Limb": - damage = limb_damage - _: - damage = torso_damage - - if damage_reduction_curve != null: - var distance_to_hit = (player_camera.global_position - collision["position"]).length()/distance - reduction = damage_reduction_curve.sample(distance_to_hit) - - hit_player.take_damage(int(float(damage) * reduction)) - - -func shoot_pellets(id:int,amount: int, limb_total_damage: int, torso_total_damage: int,head_total_damage: int, distance: float, arc: float, damage_reduction_curve: Curve = null): - if multiplayer.is_server() == false: - return - - var player: Player = player_nodes[id] - var player_camera: Camera3D = player.get_node("Camera3D") - var space: PhysicsDirectSpaceState3D = player.get_world_3d().direct_space_state - var head_damage: int = head_total_damage / amount - var torso_damage: int = torso_total_damage / amount - var limb_damage: int = limb_total_damage / amount - - for i in range(amount): - var endpoint: Vector3 = player_camera.global_position - player_camera.global_basis.z.rotated(Vector3.RIGHT,randf_range(-arc/2,arc/2)).rotated(Vector3.UP,randf_range(-arc/2,arc/2)) * distance - - var ray = PhysicsRayQueryParameters3D.create(player_camera.global_position,endpoint,1) - ray.exclude = [player.get_rid()] - ray.collide_with_areas = false - match player.team: - TEAMS.DEFENCE: - ray.collision_mask |= ATTACK_LAYER - TEAMS.ATTACK: - ray.collision_mask |= DEFENCE_LAYER - _: - ray.collision_mask |= ATTACK_LAYER | DEFENCE_LAYER - - var collision = space.intersect_ray(ray) - - - if collision != {} and collision["collider"] is Player: + if collision != {}: + if collision["collider"] is Player: var hit_player: Player = collision["collider"] var shape_object: CollisionShape3D = hit_player.shape_owner_get_owner(collision["shape"]) var reduction: float = 1 @@ -393,6 +347,71 @@ func shoot_pellets(id:int,amount: int, limb_total_damage: int, torso_total_damag reduction = damage_reduction_curve.sample(distance_to_hit) hit_player.take_damage(int(float(damage) * reduction)) + + var bullet_hole: Decal = BULLET_HOLE.instantiate() + dynamic_objects_parent.add_child(bullet_hole,true) + + var rotation_quat: Quaternion = Quaternion(Vector3.UP,collision["normal"]) + bullet_hole.quaternion *= rotation_quat + bullet_hole.global_position = collision["position"] + + + +func shoot_pellets(id:int,limb_pellet_damage: int, torso_pellet_damage: int,head_pellet_damage: int, distance: float, pellets: PackedVector2Array, damage_reduction_curve: Curve = null): + if multiplayer.is_server() == false: + return + + var amount: int = len(pellets) + + var player: Player = player_nodes[id] + var player_camera: Camera3D = player.get_node("Camera3D") + var space: PhysicsDirectSpaceState3D = player.get_world_3d().direct_space_state + + for i in range(amount): + var endpoint: Vector3 = player_camera.project_position(pellets[i],distance) + + var ray = PhysicsRayQueryParameters3D.create(player_camera.global_position,endpoint,1) + ray.exclude = [player.get_rid()] + ray.collide_with_areas = false + match player.team: + TEAMS.DEFENCE: + ray.collision_mask |= ATTACK_LAYER + TEAMS.ATTACK: + ray.collision_mask |= DEFENCE_LAYER + _: + ray.collision_mask |= ATTACK_LAYER | DEFENCE_LAYER + + var collision = space.intersect_ray(ray) + + + if collision != {}: + if collision["collider"] is Player: + var hit_player: Player = collision["collider"] + var shape_object: CollisionShape3D = hit_player.shape_owner_get_owner(collision["shape"]) + var reduction: float = 1 + var damage: int = 0 + + match shape_object.get_groups()[0]: + "Head": + damage = head_pellet_damage + "Limb": + damage = limb_pellet_damage + _: + damage = torso_pellet_damage + + if damage_reduction_curve != null: + var distance_to_hit = (player_camera.global_position - collision["position"]).length()/distance + reduction = damage_reduction_curve.sample(distance_to_hit) + + hit_player.take_damage(int(float(damage) * reduction)) + + var bullet_hole: Decal = BULLET_HOLE.instantiate() + dynamic_objects_parent.add_child(bullet_hole,true) + + var rotation_quat: Quaternion = Quaternion(Vector3.UP,collision["normal"]) + + bullet_hole.quaternion *= rotation_quat + bullet_hole.global_position = collision["position"] func interact(id: int) -> void: if multiplayer.is_server() == false: diff --git a/textures/bullet_hole.png b/textures/bullet_hole.png new file mode 100644 index 0000000..6cd86d9 Binary files /dev/null and b/textures/bullet_hole.png differ diff --git a/textures/bullet_hole.png.import b/textures/bullet_hole.png.import new file mode 100644 index 0000000..f5499f0 --- /dev/null +++ b/textures/bullet_hole.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djb2h4sf42nqq" +path="res://.godot/imported/bullet_hole.png-c5594de24f88f66a649756099fb11bb7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://textures/bullet_hole.png" +dest_files=["res://.godot/imported/bullet_hole.png-c5594de24f88f66a649756099fb11bb7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/weapons/gun/bullet_hole.tscn b/weapons/gun/bullet_hole.tscn new file mode 100644 index 0000000..d099542 --- /dev/null +++ b/weapons/gun/bullet_hole.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=3 format=3 uid="uid://u8aj6fs32ql6"] + +[ext_resource type="Texture2D" uid="uid://djb2h4sf42nqq" path="res://textures/bullet_hole.png" id="1_wpexx"] + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_wpexx"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:rotation") +properties/1/spawn = true +properties/1/replication_mode = 1 + +[node name="BulletHole" type="Decal"] +size = Vector3(0.25, 0.5, 0.25) +texture_albedo = ExtResource("1_wpexx") + +[node name="Timer" type="Timer" parent="."] +process_mode = 3 +wait_time = 60.0 +one_shot = true +autostart = true +ignore_time_scale = true + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_wpexx") + +[connection signal="timeout" from="Timer" to="." method="queue_free"] diff --git a/weapons/gun/mc/mc255.tscn b/weapons/gun/mc/mc255.tscn index 883b5c7..fd802ec 100644 --- a/weapons/gun/mc/mc255.tscn +++ b/weapons/gun/mc/mc255.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=10 format=3 uid="uid://8ohlfmr5bp0k"] +[gd_scene load_steps=12 format=3 uid="uid://8ohlfmr5bp0k"] [ext_resource type="Script" uid="uid://e6lqknfl4ngt" path="res://systems/weapon_system/weapon_substate_machine.gd" id="1_uck67"] [ext_resource type="Script" uid="uid://ofv4e3dsfe8" path="res://weapons/gun/idle_state.gd" id="2_rkf02"] [ext_resource type="Script" uid="uid://cvueeftqbxb7r" path="res://weapons/gun/semi_pellet_shoot_state.gd" id="3_jk5g7"] [ext_resource type="Script" uid="uid://hmekwp8444ao" path="res://weapons/gun/reload_state.gd" id="4_fs8hh"] [ext_resource type="Script" uid="uid://bmj0bwy2tlian" path="res://weapons/gun/intro_state.gd" id="5_3ok4b"] +[ext_resource type="Script" uid="uid://ryxe3lxtvpk4" path="res://weapons/gun/pellet_spread/pellet_spread.gd" id="6_a53f6"] [sub_resource type="Curve" id="Curve_cmn6f"] _limits = [0.0, 0.1, 0.0, 20.0] @@ -27,6 +28,12 @@ properties/1/path = NodePath(".:remaining_ammo") properties/1/spawn = true properties/1/replication_mode = 2 +[sub_resource type="Curve2D" id="Curve2D_0fc4q"] +_data = { +"points": PackedVector2Array(0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 9, -7, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -8, -3, 0, 0, 0, 0, -8, 5, 0, 0, 0, 0, -1, 7, 0, 0, 0, 0, 7, 6, 0, 0, 0, 0, 14, 6, 0, 0, 0, 0, 15, -5, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -13, -7, 0, 0, 0, 0, -10, 4, 0, 0, 0, 0, -10, 12, 0, 0, 0, 0, -1, 13, 0, 0, 0, 0, 12, 14, 0, 0, 0, 0, 6, 11, 0, 0, 0, 0, 21, 9, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 9, -16, 0, 0, 0, 0, -10, -13, 0, 0, 0, 0, -16, -2, 0, 0, 0, 0, -17, 13, 0, 0, 0, 0, -3, 19, 0, 0, 0, 0, 6, 16, 0, 0, 0, 0, 18, 16, 0, 0, 0, 0, 24, 9, 0, 0, 0, 0, 23, -7, 0, 0, 0, 0, 20, -12) +} +point_count = 28 + [node name="MC255" type="Node" node_paths=PackedStringArray("enter_state")] script = ExtResource("1_uck67") animation_prefix = &"baked_mc_" @@ -41,17 +48,15 @@ metadata/_custom_type_script = "uid://e6lqknfl4ngt" [node name="Idle" type="Node" parent="."] script = ExtResource("2_rkf02") -[node name="Shoot" type="Node" parent="." node_paths=PackedStringArray("fire_timer")] +[node name="Shoot" type="Node" parent="." node_paths=PackedStringArray("pellet_spread", "fire_timer")] script = ExtResource("3_jk5g7") vertical_curve = SubResource("Curve_cmn6f") horizontal_curve = SubResource("Curve_jk5g7") damage_reduction_curve = SubResource("Curve_bwg3m") -torso_total_damage = 100 -head_total_damage = 150 -limb_total_damage = 70 -arc = 0.08726646259971647 -max_pellets = 30 -min_pellets = 30 +torso_pellet_damage = 6 +head_pellet_damage = 24 +limb_pellet_damage = 6 +pellet_spread = NodePath("../PelletSpread") shoot_distance = 40.0 fire_timer = NodePath("../FireTimer") @@ -67,3 +72,9 @@ one_shot = true [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] replication_config = SubResource("SceneReplicationConfig_bwg3m") + +[node name="PelletSpread" type="Path2D" parent="."] +position = Vector2(640, 360) +curve = SubResource("Curve2D_0fc4q") +script = ExtResource("6_a53f6") +metadata/_custom_type_script = "uid://ryxe3lxtvpk4" diff --git a/weapons/gun/pellet_spread/icon.png b/weapons/gun/pellet_spread/icon.png new file mode 100644 index 0000000..15f6778 Binary files /dev/null and b/weapons/gun/pellet_spread/icon.png differ diff --git a/weapons/gun/pellet_spread/icon.png.import b/weapons/gun/pellet_spread/icon.png.import new file mode 100644 index 0000000..4304b37 --- /dev/null +++ b/weapons/gun/pellet_spread/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cs1dapnsaw2vw" +path="res://.godot/imported/icon.png-0bb35b0bcb159408ed12f260c09d8538.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://weapons/gun/pellet_spread/icon.png" +dest_files=["res://.godot/imported/icon.png-0bb35b0bcb159408ed12f260c09d8538.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/weapons/gun/pellet_spread/pellet_spread.gd b/weapons/gun/pellet_spread/pellet_spread.gd new file mode 100644 index 0000000..b33a611 --- /dev/null +++ b/weapons/gun/pellet_spread/pellet_spread.gd @@ -0,0 +1,13 @@ +@icon("res://weapons/gun/pellet_spread/icon.png") +class_name PelletSpread extends Path2D + +func get_dots() -> PackedVector2Array: + var result: PackedVector2Array + + for i in curve.point_count: + result.append(curve.get_point_position(i)) + + for i in range(len(result)): + result[i] += get_viewport_rect().size/2.0 + + return result diff --git a/weapons/gun/pellet_spread/pellet_spread.gd.uid b/weapons/gun/pellet_spread/pellet_spread.gd.uid new file mode 100644 index 0000000..e226298 --- /dev/null +++ b/weapons/gun/pellet_spread/pellet_spread.gd.uid @@ -0,0 +1 @@ +uid://ryxe3lxtvpk4 diff --git a/weapons/gun/pellet_spread/pellet_spread_random.gd b/weapons/gun/pellet_spread/pellet_spread_random.gd new file mode 100644 index 0000000..4bbb6d0 --- /dev/null +++ b/weapons/gun/pellet_spread/pellet_spread_random.gd @@ -0,0 +1,22 @@ +@tool +extends PelletSpread + +class_name PelletSpreadRandom + +const ASPECT = 1.7777777777777777 + +@export var random_amount: int +@export_range(0,89,0.1,"radians_as_degrees") var radius: float +@export var aspect_ratio: float = 1.7777777777777777 + +@export_tool_button("Randomize points") var randomize_button = randomize_points + +func randomize_points() -> void: + curve.clear_points() + var viewport_aspect_trasformation = Vector2(ASPECT,1) + var custom_aspect_transformation = Vector2(aspect_ratio,1./aspect_ratio) + var transformation_vector = get_viewport_rect().size / viewport_aspect_trasformation /PI + for i in range(random_amount): + var unscaled_position = Vector2(randf_range(-radius,radius),randf_range(-radius,radius)) + var scaled_position = unscaled_position*transformation_vector + curve.add_point(scaled_position) diff --git a/weapons/gun/pellet_spread/pellet_spread_random.gd.uid b/weapons/gun/pellet_spread/pellet_spread_random.gd.uid new file mode 100644 index 0000000..6a62569 --- /dev/null +++ b/weapons/gun/pellet_spread/pellet_spread_random.gd.uid @@ -0,0 +1 @@ +uid://b3cvfkvvc5c3g diff --git a/weapons/gun/semi_pellet_shoot_state.gd b/weapons/gun/semi_pellet_shoot_state.gd index 5210493..8488c8c 100644 --- a/weapons/gun/semi_pellet_shoot_state.gd +++ b/weapons/gun/semi_pellet_shoot_state.gd @@ -5,12 +5,10 @@ extends WeaponState @export var damage_reduction_curve: Curve @export var emptyable: bool -@export var torso_total_damage: int -@export var head_total_damage: int -@export var limb_total_damage: int -@export_range(0,179,0.01,"radians_as_degrees") var arc: float -@export_range(1,20,1,"or_greater") var max_pellets: int = 1 -@export_range(1,20,1,"or_greater") var min_pellets: int = 1 +@export var torso_pellet_damage: int +@export var head_pellet_damage: int +@export var limb_pellet_damage: int +@export var pellet_spread: PelletSpread @export var shoot_distance: float = 100 @export var fire_timer: Timer @@ -44,8 +42,7 @@ func fire() -> void: machine.animation_player.play(with_morphems("shoot")) if is_multiplayer_authority(): - var pellets: int = randi_range(min_pellets,max_pellets) - Session.shoot_pellets(int(machine.player.name),pellets,limb_total_damage,torso_total_damage,head_total_damage,shoot_distance,arc,damage_reduction_curve) + Session.shoot_pellets(int(machine.player.name),limb_pellet_damage,torso_pellet_damage,head_pellet_damage,shoot_distance,pellet_spread.get_dots(),damage_reduction_curve) machine.player.get_node("ShootAudio").multiplayer_play() fire_timer.start()