knockback forge implemented

This commit is contained in:
2026-05-04 16:22:30 +02:00
parent b2b7baffe8
commit a139990390
21 changed files with 194 additions and 70 deletions

View File

@@ -6,20 +6,27 @@ using Godot;
namespace Movementtests.forge.abilities; namespace Movementtests.forge.abilities;
public record SimpleHitEffectData(Vector3 SourceLocation, Vector3 TargetLocation);
public class SimpleHitBehavior(ForgeEffectData? damage) : IAbilityBehavior public class SimpleHitBehavior(ForgeEffectData? damage) : IAbilityBehavior
{ {
public void OnStarted(AbilityBehaviorContext context) public void OnStarted(AbilityBehaviorContext context)
{ {
if (context.Target == null || damage == null) return; if (context.Target == null || damage == null) return;
var effect = new Effect(damage.GetEffectData(), new EffectOwnership(context.Owner, context.Owner)); var sourceLocation = (context.Source as Node3D)?.GlobalPosition ?? Vector3.Zero;
context.Target.EffectsManager.ApplyEffect(effect); var targetLocation = (context.Target as Node3D)?.GlobalPosition ?? Vector3.Zero;
context.AbilityHandle.CommitAbility(); var effect = new Effect(damage.GetEffectData(), new EffectOwnership(context.Owner, context.Owner));
context.InstanceHandle.End(); context.Target.EffectsManager.ApplyEffect(effect, new SimpleHitEffectData(sourceLocation, targetLocation));
// context.InstanceHandle.End();
} }
public void OnEnded(AbilityBehaviorContext context) {} public void OnEnded(AbilityBehaviorContext context)
{
context.AbilityHandle.CommitAbility();
}
} }
[Tool] [Tool]

View File

@@ -0,0 +1,102 @@
using System.Collections.Generic;
using Gamesmiths.Forge.Core;
using Gamesmiths.Forge.Effects;
using Gamesmiths.Forge.Effects.Calculator;
using Gamesmiths.Forge.Effects.Magnitudes;
using Gamesmiths.Forge.Events;
using Gamesmiths.Forge.Godot.Resources;
using Gamesmiths.Forge.Godot.Resources.Calculators;
using Godot;
using Movementtests.forge.abilities;
using Movementtests.scenes.components.knockback;
namespace Movementtests.tools.calculators;
public record KnockbackDone(float KnockbackValue, Vector3 knockbackDirection);
public class KnockbackExecution : CustomExecution
{
private readonly RKnockback _knockback;
private readonly ForgeTag _knockbackTag;
private readonly ForgeTagContainer? _knockbackReceiverEventTags;
private readonly ForgeTagContainer? _knockbackDealerEventTags;
public AttributeCaptureDefinition TargetIncomingDamage { get; }
public KnockbackExecution(ForgeTag knockbackTag, RKnockback knockback, ForgeTagContainer? knockbackDealerEventTags, ForgeTagContainer? knockbackReceiverEventTags)
{
_knockback = knockback;
_knockbackTag = knockbackTag;
_knockbackDealerEventTags = knockbackDealerEventTags;
_knockbackReceiverEventTags = knockbackReceiverEventTags;
TargetIncomingDamage = new AttributeCaptureDefinition(
"MetaAttributeSet.IncomingDamage",
AttributeCaptureSource.Target);
AttributesToCapture.Add(TargetIncomingDamage);
}
public override ModifierEvaluatedData[] EvaluateExecution(Effect effect, IForgeEntity target, EffectEvaluatedData? effectEvaluatedData)
{
var results = new List<ModifierEvaluatedData>();
if (!target.Tags.CombinedTags.HasTag(_knockbackTag.GetTag()))
return [.. results];
float targetIncomingDamage = CaptureAttributeMagnitude(
TargetIncomingDamage,
effect,
target,
effectEvaluatedData);
if (targetIncomingDamage <= 0)
return [.. results];
var knockbackValue = _knockback.Modifier * targetIncomingDamage / 100.0f;
if (_knockbackReceiverEventTags is null)
return [.. results];
var knockbackDirection = Vector3.Zero;
if (effectEvaluatedData?.TryGetContextData(out SimpleHitEffectData? hitEffectData) == true)
{
knockbackDirection = hitEffectData.SourceLocation.DirectionTo(hitEffectData.TargetLocation);
}
target.Events.Raise(new EventData<KnockbackDone>
{
EventTags = _knockbackReceiverEventTags.GetTagContainer(),
Source = effect.Ownership.Owner,
Target = target,
EventMagnitude = knockbackValue,
Payload = new KnockbackDone(knockbackValue, knockbackDirection)
});
if (effect.Ownership.Source is null || _knockbackDealerEventTags is null)
return [.. results];
effect.Ownership.Source.Events.Raise(new EventData<KnockbackDone>
{
EventTags = _knockbackDealerEventTags.GetTagContainer(),
Source = effect.Ownership.Owner,
Target = target,
EventMagnitude = knockbackValue,
Payload = new KnockbackDone(knockbackValue, knockbackDirection)
});
return [.. results];
}
}
[GlobalClass]
public partial class ForgeKnockbackExecution : ForgeCustomExecution
{
[Export] public ForgeTag? KnockbackableTag { get; set; }
[Export] public RKnockback? Knockback { get; set; }
[Export] public ForgeTagContainer? KnockbackDealerEventTags { get; set; }
[Export] public ForgeTagContainer? KnockbackReceiverEventTags { get; set; }
public override CustomExecution GetExecutionClass()
{
if (Knockback == null || KnockbackableTag == null) throw new System.ArgumentException("Knockback or KnockbackableTag is null");
return new KnockbackExecution(KnockbackableTag, Knockback, KnockbackDealerEventTags, KnockbackReceiverEventTags);
}
}

View File

@@ -0,0 +1 @@
uid://diondfg5xp78h

View File

@@ -4,4 +4,4 @@
[resource] [resource]
script = ExtResource("1_l686n") script = ExtResource("1_l686n")
RegisteredTags = Array[String](["character.player", "character.enemy", "weapon", "status.stunned", "status.burning", "status.frozen", "abilities.weapon.land", "abilities.weapon.flying", "abilities.weapon.left", "events.combat.damage", "events.combat.hit", "events.weapon.flyingTick", "events.weapon.startedFlying", "events.weapon.stoppedFlying", "events.weapon.handToFlying", "events.weapon.flyingToHand", "events.weapon.plantedToHand", "events.weapon.plantedToFlying", "events.weapon.planted", "cooldown.empoweredAction", "cooldown.empoweredSwordThrow", "cues.resources.mana", "events.player.empowered_action_used", "cues.resources.mana.inhibited", "cues.resources.health", "cooldown.enemy.hit", "events.combat.death", "cooldown.hit", "events.player.hit", "cues.enemy.health", "immunity.damage", "status", "traits.damageable"]) RegisteredTags = Array[String](["character.player", "character.enemy", "weapon", "status.stunned", "status.burning", "status.frozen", "abilities.weapon.land", "abilities.weapon.flying", "abilities.weapon.left", "events.combat.damage", "events.combat.hit", "events.weapon.flyingTick", "events.weapon.startedFlying", "events.weapon.stoppedFlying", "events.weapon.handToFlying", "events.weapon.flyingToHand", "events.weapon.plantedToHand", "events.weapon.plantedToFlying", "events.weapon.planted", "cooldown.empoweredAction", "cooldown.empoweredSwordThrow", "cues.resources.mana", "events.player.empowered_action_used", "cues.resources.mana.inhibited", "cues.resources.health", "cooldown.enemy.hit", "events.combat.death", "cooldown.hit", "events.player.hit", "cues.enemy.health", "immunity.damage", "status", "traits.damageable", "traits.knockbackable", "events.combat.knockback_dealt", "events.combat.knockback_received"])

View File

@@ -8,6 +8,7 @@
[ext_resource type="Script" uid="uid://dngf30hxy5go4" path="res://addons/forge/resources/components/ModifierTags.cs" id="2_jwyed"] [ext_resource type="Script" uid="uid://dngf30hxy5go4" path="res://addons/forge/resources/components/ModifierTags.cs" id="2_jwyed"]
[ext_resource type="Resource" uid="uid://4rkwr10pc6tp" path="res://forge/resources/custom_executions/physical_damage_calculator.tres" id="2_l5emy"] [ext_resource type="Resource" uid="uid://4rkwr10pc6tp" path="res://forge/resources/custom_executions/physical_damage_calculator.tres" id="2_l5emy"]
[ext_resource type="Script" uid="uid://bdfcavbjyhxxa" path="res://addons/forge/resources/ForgeModifier.cs" id="3_c4wry"] [ext_resource type="Script" uid="uid://bdfcavbjyhxxa" path="res://addons/forge/resources/ForgeModifier.cs" id="3_c4wry"]
[ext_resource type="Resource" uid="uid://cc1qrmbp12fk8" path="res://forge/resources/custom_executions/player_hit_knoback_calculation.tres" id="3_l5emy"]
[ext_resource type="Script" uid="uid://dhxfbxh54pyxp" path="res://addons/forge/resources/abilities/ForgeAbilityData.cs" id="3_w1wo0"] [ext_resource type="Script" uid="uid://dhxfbxh54pyxp" path="res://addons/forge/resources/abilities/ForgeAbilityData.cs" id="3_w1wo0"]
[ext_resource type="Script" uid="uid://cn3b4ya15fg7e" path="res://addons/forge/resources/magnitudes/ForgeScalableFloat.cs" id="4_c4wry"] [ext_resource type="Script" uid="uid://cn3b4ya15fg7e" path="res://addons/forge/resources/magnitudes/ForgeScalableFloat.cs" id="4_c4wry"]
[ext_resource type="Script" uid="uid://2gm1hdhi8u08" path="res://addons/forge/resources/magnitudes/ForgeModifierMagnitude.cs" id="5_0cyim"] [ext_resource type="Script" uid="uid://2gm1hdhi8u08" path="res://addons/forge/resources/magnitudes/ForgeModifierMagnitude.cs" id="5_0cyim"]
@@ -63,7 +64,7 @@ script = ExtResource("2_5vjbv")
Name = "Player Hit Effect" Name = "Player Hit Effect"
Modifiers = Array[Object]([SubResource("Resource_04hqa")]) Modifiers = Array[Object]([SubResource("Resource_04hqa")])
Components = Array[Object]([ExtResource("1_r7waw")]) Components = Array[Object]([ExtResource("1_r7waw")])
Executions = Array[Object]([ExtResource("2_l5emy")]) Executions = Array[Object]([ExtResource("2_l5emy"), ExtResource("3_l5emy")])
StackLimit = SubResource("Resource_8fbeq") StackLimit = SubResource("Resource_8fbeq")
InitialStack = SubResource("Resource_0cyim") InitialStack = SubResource("Resource_0cyim")
Cues = [] Cues = []

View File

@@ -0,0 +1,20 @@
[gd_resource type="Resource" script_class="ForgeKnockbackExecution" format=3 uid="uid://cc1qrmbp12fk8"]
[ext_resource type="Script" uid="uid://b44cse62qru7j" path="res://scenes/components/knockback/RKnockback.cs" id="1_kcl5u"]
[ext_resource type="Resource" uid="uid://bhn27s8ne0uyg" path="res://forge/resources/tag_containers/on_knockback_dealt.tres" id="2_oqtq1"]
[ext_resource type="Resource" uid="uid://bkr6uu57wm3o3" path="res://forge/resources/tag_containers/on_knockback_received.tres" id="3_1va1b"]
[ext_resource type="Resource" uid="uid://45l7vnfs72b" path="res://forge/resources/tag_containers/knockbackable_tag.tres" id="4_0i0oh"]
[ext_resource type="Script" uid="uid://diondfg5xp78h" path="res://forge/calculators/ForgeKnockbackExecution.cs" id="5_babc1"]
[sub_resource type="Resource" id="Resource_6x2ov"]
script = ExtResource("1_kcl5u")
Modifier = 50.0
metadata/_custom_type_script = "uid://b44cse62qru7j"
[resource]
script = ExtResource("5_babc1")
KnockbackableTag = ExtResource("4_0i0oh")
Knockback = SubResource("Resource_6x2ov")
KnockbackDealerEventTags = ExtResource("2_oqtq1")
KnockbackReceiverEventTags = ExtResource("3_1va1b")
metadata/_custom_type_script = "uid://diondfg5xp78h"

View File

@@ -4,5 +4,5 @@
[resource] [resource]
script = ExtResource("1_kdy2b") script = ExtResource("1_kdy2b")
ContainerTags = Array[String](["character.enemy", "traits.damageable"]) ContainerTags = Array[String](["character.enemy", "traits.damageable", "traits.knockbackable"])
metadata/_custom_type_script = "uid://cw525n4mjqgw0" metadata/_custom_type_script = "uid://cw525n4mjqgw0"

View File

@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ForgeTag" format=3 uid="uid://45l7vnfs72b"]
[ext_resource type="Script" uid="uid://dpakv7agvir6y" path="res://addons/forge/resources/ForgeTag.cs" id="1_1cy5u"]
[resource]
script = ExtResource("1_1cy5u")
Tag = "traits.knockbackable"
metadata/_custom_type_script = "uid://dpakv7agvir6y"

View File

@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ForgeTagContainer" format=3 uid="uid://bhn27s8ne0uyg"]
[ext_resource type="Script" uid="uid://cw525n4mjqgw0" path="res://addons/forge/resources/ForgeTagContainer.cs" id="1_kgxiq"]
[resource]
script = ExtResource("1_kgxiq")
ContainerTags = Array[String](["events.combat.knockback_dealt"])
metadata/_custom_type_script = "uid://cw525n4mjqgw0"

View File

@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ForgeTagContainer" format=3 uid="uid://bkr6uu57wm3o3"]
[ext_resource type="Script" uid="uid://cw525n4mjqgw0" path="res://addons/forge/resources/ForgeTagContainer.cs" id="1_ro1gp"]
[resource]
script = ExtResource("1_ro1gp")
ContainerTags = Array[String](["events.combat.knockback_received"])
metadata/_custom_type_script = "uid://cw525n4mjqgw0"

View File

@@ -1,8 +1,9 @@
using Godot; using Godot;
using Movementtests.scenes.components.knockback;
namespace Movementtests.interfaces; namespace Movementtests.interfaces;
public record KnockbackRecord(DamageRecord DamageRecord, float ForceMultiplier = 1.0f); public record KnockbackRecord(Vector3 Direction, float ForceMultiplier = 1.0f);
public interface IKnockbackable public interface IKnockbackable
{ {

View File

@@ -74,7 +74,6 @@ HealthInputs = ExtResource("11_5jlg7")
DamageInputs = ExtResource("12_pjgox") DamageInputs = ExtResource("12_pjgox")
Target = NodePath("../Player") Target = NodePath("../Player")
SpawnInterval = 5.0 SpawnInterval = 5.0
IsActiveOnStart = false
[node name="FlyingSpawner2" parent="." index="12" unique_id=365997644 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")] [node name="FlyingSpawner2" parent="." index="12" unique_id=365997644 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45.5, 25.5, -42.5) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45.5, 25.5, -42.5)
@@ -84,7 +83,6 @@ HealthInputs = ExtResource("11_5jlg7")
DamageInputs = ExtResource("12_pjgox") DamageInputs = ExtResource("12_pjgox")
Target = NodePath("../Player") Target = NodePath("../Player")
SpawnInterval = 5.0 SpawnInterval = 5.0
IsActiveOnStart = false
[node name="Targets" type="Node3D" parent="." index="13" unique_id=1620747784] [node name="Targets" type="Node3D" parent="." index="13" unique_id=1620747784]

View File

@@ -1,13 +1,14 @@
using Godot; using Godot;
using System;
using Movementtests.interfaces; using Movementtests.interfaces;
namespace Movementtests.scenes.components.knockback;
[GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_wind.png")] [GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_wind.png")]
public partial class CKnockback : Node3D, IKnockbackable public partial class CKnockback : Node3D, IKnockbackable
{ {
[Export] public RKnockback RKnockback { get; set;} = null!; [Export] public RKnockback RKnockback { get; set;} = null!;
private KnockbackRecord _knockbackRecord = null!; private KnockbackRecord? _knockbackRecord;
public void RegisterKnockback(KnockbackRecord knockbackRecord) public void RegisterKnockback(KnockbackRecord knockbackRecord)
{ {
@@ -18,9 +19,8 @@ public partial class CKnockback : Node3D, IKnockbackable
{ {
if (_knockbackRecord == null) return Vector3.Zero; if (_knockbackRecord == null) return Vector3.Zero;
var knockbackDirection = GlobalPosition - _knockbackRecord.DamageRecord.SourceLocation; var finalKnockback = _knockbackRecord.Direction.Normalized() * RKnockback.Modifier * _knockbackRecord.ForceMultiplier;
var finalKnockback = knockbackDirection.Normalized() * RKnockback.Modifier * _knockbackRecord.ForceMultiplier; _knockbackRecord = null;
_knockbackRecord = null!;
return finalKnockback; return finalKnockback;
} }
} }

View File

@@ -1,16 +1,12 @@
using Godot; using Godot;
using System;
using Movementtests.interfaces; namespace Movementtests.scenes.components.knockback;
[GlobalClass, Icon("res://assets/ui/IconGodotNode/white/icon_wind.png")] [GlobalClass, Icon("res://assets/ui/IconGodotNode/white/icon_wind.png")]
public partial class RKnockback : Resource public partial class RKnockback(float modifier) : Resource
{ {
[Export] [Export]
public float Modifier { get; set;} public float Modifier { get; set;} = modifier;
public RKnockback() : this(1.0f) {} public RKnockback() : this(1.0f) {}
public RKnockback(float modifier)
{
Modifier = modifier;
}
} }

View File

@@ -13,6 +13,7 @@ using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Tags; using Gamesmiths.Forge.Tags;
using Godot; using Godot;
using Movementtests.interfaces; using Movementtests.interfaces;
using Movementtests.scenes.components.knockback;
using Movementtests.systems; using Movementtests.systems;
using Movementtests.tools; using Movementtests.tools;
using Movementtests.tools.calculators; using Movementtests.tools.calculators;
@@ -165,9 +166,15 @@ public partial class Enemy : CharacterBody3D,
Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.hit"), Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.hit"),
data => {GD.Print("Hit!");}); data => {GD.Print("Hit!");});
Events.Subscribe<DamageDone>(Tag.RequestTag(TagsManager, "events.combat.damage"), OnDamageReceived); Events.Subscribe<DamageDone>(Tag.RequestTag(TagsManager, "events.combat.damage"), OnDamageReceived);
Events.Subscribe<KnockbackDone>(Tag.RequestTag(TagsManager, "events.combat.knockback_received"), OnKnockbackReceived);
Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.death"), OnDeath); Events.Subscribe(Tag.RequestTag(TagsManager, "events.combat.death"), OnDeath);
} }
public void OnKnockbackReceived(EventData<KnockbackDone> data)
{
RegisterKnockback(new KnockbackRecord(data.Payload.knockbackDirection, data.EventMagnitude));
}
public void SetupSignals() public void SetupSignals()
{ {
// Anonymous function call to erase return values of ReduceHealth // Anonymous function call to erase return values of ReduceHealth

View File

@@ -4,5 +4,5 @@
[resource] [resource]
script = ExtResource("1_yq03x") script = ExtResource("1_yq03x")
Modifier = 20.0 Modifier = 2.0
metadata/_custom_type_script = "uid://b44cse62qru7j" metadata/_custom_type_script = "uid://b44cse62qru7j"

View File

@@ -4,5 +4,5 @@
[resource] [resource]
script = ExtResource("1_vdia8") script = ExtResource("1_vdia8")
Modifier = 30.0 Modifier = 1.0
metadata/_custom_type_script = "uid://b44cse62qru7j" metadata/_custom_type_script = "uid://b44cse62qru7j"

View File

@@ -4,7 +4,7 @@
[resource] [resource]
script = ExtResource("1_hsy8g") script = ExtResource("1_hsy8g")
Speed = 5.0 Speed = 4.0
Acceleration = 3.0 Acceleration = 1.0
GravityModifier = 5.0 GravityModifier = 5.0
metadata/_custom_type_script = "uid://dtpxijlnb2c5" metadata/_custom_type_script = "uid://dtpxijlnb2c5"

View File

@@ -26,6 +26,7 @@ using Movementtests.interfaces;
using Movementtests.systems; using Movementtests.systems;
using Movementtests.player_controller.Scripts; using Movementtests.player_controller.Scripts;
using Movementtests.managers; using Movementtests.managers;
using Movementtests.scenes.components.knockback;
using Movementtests.tools; using Movementtests.tools;
using Movementtests.tools.calculators; using Movementtests.tools.calculators;
using RustyOptions; using RustyOptions;
@@ -150,7 +151,7 @@ public partial class PlayerController : CharacterBody3D, IForgeEntity, ICueHandl
public float AimAssistReductionStartDistance { get; set; } = 10f; public float AimAssistReductionStartDistance { get; set; } = 10f;
[ExportGroup("Damage")] [Export] public RDamage RDamage { get; set; } = null!; [ExportGroup("Damage")] [Export] public RDamage RDamage { get; set; } = null!;
[Export] public RKnockback? RKnockback { get; set; } = null!; [Export] public RKnockback? RKnockback { get; set; }
[ExportGroup("Targeting")] [ExportGroup("Targeting")]
[Export(PropertyHint.Range, "0,20,0.1,or_greater")] [Export(PropertyHint.Range, "0,20,0.1,or_greater")]
@@ -2555,7 +2556,7 @@ public partial class PlayerController : CharacterBody3D, IForgeEntity, ICueHandl
public void ManualKnockback() public void ManualKnockback()
{ {
Velocity = -_dashDirection*RKnockback.Modifier; Velocity = -_dashDirection*RKnockback!.Modifier;
} }
public static Vector3 ComputePositionAfterTargetedDash(Vector3 targetLocation, Vector3 targetHitLocation) public static Vector3 ComputePositionAfterTargetedDash(Vector3 targetLocation, Vector3 targetHitLocation)
@@ -2628,11 +2629,10 @@ public partial class PlayerController : CharacterBody3D, IForgeEntity, ICueHandl
foreach (var entity in _hitEnemies) foreach (var entity in _hitEnemies)
{ {
// TODO: WTF why doesn't health move _hitAbilityHandle.Activate(out var flags, entity);
// GD.Print(entity.Attributes["EnemyAttributeSet.Health"].CurrentValue);
_hitAbilityHandle.Activate(out _, entity);
} }
_hitEnemies.Clear(); _hitEnemies.Clear();
_hitAbilityHandle.Cancel();
HeadSystem.OnHitTarget(); HeadSystem.OnHitTarget();
_audioStream.SwitchToClipByName("hits"); _audioStream.SwitchToClipByName("hits");

View File

@@ -1,32 +0,0 @@
using Godot;
using GdUnit4;
using static GdUnit4.Assertions;
using Movementtests.interfaces;
using Movementtests.systems.damage;
namespace Movementtests.tests;
[TestSuite, RequireGodotRuntime]
public class KnockbackComponentUnitTest
{
[TestCase]
public void RegisterAndComputeKnockback()
{
var cKnock = new CKnockback();
cKnock.RKnockback = new RKnockback(2.0f);
cKnock.GlobalPosition = Vector3.Zero;
var damage = new DamageRecord(new Vector3(10, 0, 0), new RDamage(0, EDamageTypes.Normal));
var record = new KnockbackRecord(damage, 1.5f);
cKnock.RegisterKnockback(record);
var force = cKnock.ComputeKnockback();
// Direction from source(10,0,0) to target(0,0,0) is (-1,0,0), scaled by modifier(2) and multiplier(1.5) => (-3,0,0)
AssertVector(force).IsEqual(new Vector3(-3, 0, 0));
// Second call returns zero since internal state resets
var second = cKnock.ComputeKnockback();
AssertVector(second).IsEqual(Vector3.Zero);
}
}

View File

@@ -1 +0,0 @@
uid://bv0eionbgbig5