diff --git a/internal/characters/ororon/asc.go b/internal/characters/ororon/asc.go index bdadce81bb..810b6f0053 100644 --- a/internal/characters/ororon/asc.go +++ b/internal/characters/ororon/asc.go @@ -97,9 +97,8 @@ func (c *char) a1NightSoulAttack(atk *combat.AttackEvent) { return } c.AddStatus(a1DamageIcdKey, 1.8*60, true) - if !c.nightsoulState.HasBlessing() { - c.a1EnterBlessing() - } + c.a1EnterBlessing() + c.nightsoulState.ConsumePoints(10) c.hypersense(1.6, a1Abil, atk.Pattern.Shape.Pos()) } @@ -137,9 +136,25 @@ func (c *char) hypersense(mult float64, abil string, initialTargetPos geometry.P c.c6onHypersense() } +// When Ororon has the jump blessing, do nothing. Blessing will exit when jump is done. +func (c *char) a1ExitBlessing() { + c.inA1Blessing = false + if !c.inTransmissionBlessing { + c.nightsoulState.ExitBlessing() + } +} + func (c *char) a1EnterBlessing() { c.nightsoulState.EnterBlessing(c.nightsoulState.Points()) - c.QueueCharTask(c.nightsoulState.ExitBlessing, 6*60) + c.inA1Blessing = true + c.a1Src = c.Core.F + src := c.a1Src + c.QueueCharTask(func() { + if src != c.a1Src { + return + } + c.a1ExitBlessing() + }, 6*60) } func (c *char) a1OnSkill() { diff --git a/internal/characters/ororon/config.yml b/internal/characters/ororon/config.yml index 650f569a32..8900e40ef3 100644 --- a/internal/characters/ororon/config.yml +++ b/internal/characters/ororon/config.yml @@ -10,6 +10,10 @@ action_param_keys: - param: "weakspot" skill: - param: "travel" + jump: + - param: "hold" + high_plunge: + - param: "fall" skill_data_mapping: attack: aim: diff --git a/internal/characters/ororon/jump.go b/internal/characters/ororon/jump.go new file mode 100644 index 0000000000..9bdf73d9d6 --- /dev/null +++ b/internal/characters/ororon/jump.go @@ -0,0 +1,105 @@ +package ororon + +import ( + "github.com/genshinsim/gcsim/internal/frames" + "github.com/genshinsim/gcsim/pkg/core/action" + "github.com/genshinsim/gcsim/pkg/core/event" + "github.com/genshinsim/gcsim/pkg/core/player" +) + +var jumpHoldFrames [][]int + +func init() { + // Hold Jump + jumpHoldFrames = make([][]int, 2) + // Hold Jump -> X + jumpHoldFrames[0] = frames.InitAbilSlice(60 * 10) // set to very high number for most abilities + jumpHoldFrames[0][action.ActionHighPlunge] = plungeCancelFrames + + // Fall -> X + jumpHoldFrames[1] = frames.InitAbilSlice(47) + jumpHoldFrames[1][action.ActionAttack] = 45 + jumpHoldFrames[1][action.ActionAim] = 46 + jumpHoldFrames[1][action.ActionSkill] = 45 + jumpHoldFrames[1][action.ActionBurst] = 46 + jumpHoldFrames[1][action.ActionDash] = 46 + jumpHoldFrames[1][action.ActionJump] = 45 + jumpHoldFrames[1][action.ActionWalk] = 47 + jumpHoldFrames[1][action.ActionSwap] = 44 +} + +func (c *char) exitJumpBlessing() { + c.inTransmissionBlessing = false + if !c.inA1Blessing { + c.nightsoulState.ExitBlessing() + } +} + +// Add NS status. +// Set a CB to cancel high jump if max duration exceeded. +// Grant airborne status. +// Consume stamina. +// Hold defines when fall action will automatically be called. +func (c *char) highJump(hold int, p map[string]int) (action.Info, error) { + if (hold > maxJumpFrames-fallCancelFrames) || (hold < 0) { + hold = maxJumpFrames - fallCancelFrames + } + + jumpDur := fallCancelFrames + hold + c.jmpSrc = c.Core.F + src := c.jmpSrc + c.Core.Player.SetAirborne(player.AirborneOroron) + + // Don't add NS if jump is cancelled before NS would be added. + c.QueueCharTask(func() { + if src != c.jmpSrc { + return + } + c.nightsoulState.EnterBlessing(c.nightsoulState.Points()) + c.inTransmissionBlessing = true + }, jumpNsDelay) + + // Consume stamina. + c.QueueCharTask(func() { + h := c.Core.Player + // Apply stamina reduction mods. + stamDrain := h.AbilStamCost(c.Index, action.ActionJump, p) + h.Stam -= stamDrain + if h.Stam < 0 { + h.Stam = 0 + } + // While in high jump, ororon cannot start resuming stamina regen until after landing. + h.LastStamUse = *h.F + jumpDur + fallFrames + h.Events.Emit(event.OnStamUse, action.ActionJump) + }, jumpStamDrainDelay) + + act := action.Info{ + Frames: frames.NewAbilFunc(jumpHoldFrames[0]), + AnimationLength: jumpDur + jumpHoldFrames[1][action.ActionWalk], + CanQueueAfter: plungeCancelFrames, // earliest cancel + State: action.JumpState, + } + + // Trigger a fall after max jump duration + // TODO: Is this hitlag extended? Does this skip if the action is canceled? + act.QueueAction(func() { + if src != c.jmpSrc { + return + } + + fallParam := map[string]int{"fall": 1} + // Ideally this would inject the action into the queue of actions to take from the config file, rather than calling exec directly + c.Core.Player.Exec(action.ActionHighPlunge, c.Base.Key, fallParam) + }, jumpDur) + // c.QueueCharTask(fallCb, jumpDur) + return act, nil +} + +// TODO: How does it work if xinyuan airborne buff is active and hold jump is used? +func (c *char) Jump(p map[string]int) (action.Info, error) { + hold := p["hold"] + if hold == 0 { + return c.Character.Jump(p) + } + return c.highJump(hold-1, p) +} diff --git a/internal/characters/ororon/ororon.go b/internal/characters/ororon/ororon.go index a375374ad4..9d3c74d250 100644 --- a/internal/characters/ororon/ororon.go +++ b/internal/characters/ororon/ororon.go @@ -4,6 +4,7 @@ import ( tmpl "github.com/genshinsim/gcsim/internal/template/character" "github.com/genshinsim/gcsim/internal/template/nightsoul" "github.com/genshinsim/gcsim/pkg/core" + "github.com/genshinsim/gcsim/pkg/core/action" "github.com/genshinsim/gcsim/pkg/core/info" "github.com/genshinsim/gcsim/pkg/core/keys" "github.com/genshinsim/gcsim/pkg/core/player/character" @@ -11,17 +12,36 @@ import ( "github.com/genshinsim/gcsim/pkg/model" ) +const ( + superJumpBeginFrames = 15 // Jump->SuperJump Frames + + jumpNsDelay = 15 // From swap ui gray to nightsoul state outline appears + jumpStamDrainDelay = 5 + jumpStamDrainAmt = 75 + jumpStamReqAmt = 1 + + maxJumpFrames = 162 // From swap ui gray to glider wings appear + plungeCancelFrames = superJumpBeginFrames + 18 // From start of jump animation to plunge animation start. Earliest possible plunge cancel. + fallCancelFrames = superJumpBeginFrames + 46 // From From start of jump animation to UI changes from gliding to standard UI. Earliest possible cancel. + + fallFrames = 44 // From fall animation start to swap icon un-gray. +) + func init() { core.RegisterCharFunc(keys.Ororon, NewChar) } type char struct { *tmpl.Character - nightsoulState *nightsoul.State - particlesGenerated bool - c2Bonus []float64 - c6stacks *stacks.MultipleRefreshNoRemove - c6bonus []float64 + nightsoulState *nightsoul.State + particlesGenerated bool + c2Bonus []float64 + c6stacks *stacks.MultipleRefreshNoRemove + c6bonus []float64 + a1Src int + jmpSrc int + inA1Blessing bool + inTransmissionBlessing bool } func NewChar(s *core.Core, w *character.CharWrapper, _ info.CharacterProfile) error { @@ -55,3 +75,20 @@ func (c *char) AnimationStartDelay(k model.AnimationDelayKey) int { } return c.Character.AnimationStartDelay(k) } + +func (c *char) ActionStam(a action.Action, p map[string]int) float64 { + if a == action.ActionJump && p["hold"] != 0 { + return 75 + } + return c.Character.ActionStam(a, p) +} + +func (c *char) ActionReady(a action.Action, p map[string]int) (bool, action.Failure) { + // check if a1 window is active is on-field + if a == action.ActionJump && p["hold"] != 0 { + if c.Core.Player.Stam < jumpStamReqAmt { + return false, action.InsufficientStamina + } + } + return c.Character.ActionReady(a, p) +} diff --git a/internal/characters/ororon/ororon_gen.go b/internal/characters/ororon/ororon_gen.go index df306ca8fd..028899b764 100644 --- a/internal/characters/ororon/ororon_gen.go +++ b/internal/characters/ororon/ororon_gen.go @@ -201,4 +201,32 @@ var ( 0.747, 0.7885, } + + collision = []float64{ + 0.5683, + 0.6145, + 0.6608, + 0.7269, + 0.7731, + 0.826, + 0.8987, + 0.9714, + 1.0441, + 1.1234, + 1.2027, + } + + highPlunge = []float64{ + 1.4193, + 1.5349, + 1.6504, + 1.8154, + 1.931, + 2.063, + 2.2445, + 2.4261, + 2.6076, + 2.8057, + 3.0037, + } ) diff --git a/internal/characters/ororon/plunge.go b/internal/characters/ororon/plunge.go new file mode 100644 index 0000000000..82cb5eca31 --- /dev/null +++ b/internal/characters/ororon/plunge.go @@ -0,0 +1,155 @@ +package ororon + +import ( + "fmt" + + "github.com/genshinsim/gcsim/internal/frames" + "github.com/genshinsim/gcsim/pkg/core/action" + "github.com/genshinsim/gcsim/pkg/core/attacks" + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/combat" + "github.com/genshinsim/gcsim/pkg/core/geometry" + "github.com/genshinsim/gcsim/pkg/core/glog" + "github.com/genshinsim/gcsim/pkg/core/player" +) + +const highPlungeHitmark = 52 +const collisionHitmark = 51 + +// const highPlungePoiseDMG = 100.0 // Not needed since dmg type is pierce? +// const collisionPoiseDMG = 10.0 + +const highPlungeRadius = 3.5 +const collisionRadius = 1.0 + +var plungeFrames []int + +func init() { + // Plunge -> X + plungeFrames = frames.InitAbilSlice(66) // Default is From plunge animation start to swap icon un-gray + plungeFrames[action.ActionAttack] = 68 + plungeFrames[action.ActionAim] = 66 + plungeFrames[action.ActionSkill] = 65 + plungeFrames[action.ActionBurst] = 65 + plungeFrames[action.ActionDash] = 53 + plungeFrames[action.ActionJump] = 82 + plungeFrames[action.ActionWalk] = 80 + plungeFrames[action.ActionSwap] = 66 +} + +func (c *char) fall() action.Info { + // Fall cancel can't happen until after high_plunge can happen. Delay all side effects if try to fall cancel too early. + delay := fallCancelFrames - (c.Core.F - c.jmpSrc) + + // Cleanup high jump. + if delay <= 0 { + delay = 0 + c.exitJumpBlessing() + } else { + c.Core.Log.NewEvent( + fmt.Sprintf("Fall cancel cannot begin until %d frames after jump start; delaying fall by %d frames", fallCancelFrames, delay), + glog.LogCooldownEvent, + c.Index) + + c.QueueCharTask(c.exitJumpBlessing, delay) + } + // Allow stam to start regen when landing + c.Core.Player.LastStamUse = c.Core.F + jumpHoldFrames[1][action.ActionSwap] + delay + + return action.Info{ + Frames: func(next action.Action) int { + return frames.NewAbilFunc(jumpHoldFrames[1])(next) + delay + }, + // Is this supposed to be whatever the max over Frames is? + AnimationLength: jumpHoldFrames[1][action.ActionWalk] + delay, + CanQueueAfter: jumpHoldFrames[1][action.ActionSwap] + delay, + State: action.JumpState, + } +} + +// Plunge normal falling attack damage queue generator +// Standard - Always part of high/low plunge attacks +func (c *char) plungeCollision(delay int) { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Plunge Collision", + AttackTag: attacks.AttackTagPlunge, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Physical, + Durability: 0, + Mult: collision[c.TalentLvlAttack()], + } + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{}, + collisionRadius), + delay, + delay) +} + +// High Plunge attack damage queue generator +// Use the "collision" optional argument if you want to do a falling hit on the way down +// Default = 0 +func (c *char) HighPlungeAirborneOroron(p map[string]int) (action.Info, error) { + // Cleanup high jump. + c.exitJumpBlessing() + // Allow player to resume stam as soon as plunge is initiated + c.Core.Player.LastStamUse = c.Core.F + + collision := p["collision"] + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "High Plunge", + AttackTag: attacks.AttackTagPlunge, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Physical, + Durability: 25, + Mult: highPlunge[c.TalentLvlAttack()], + UseDef: true, + } + + if collision > 0 { + c.plungeCollision(collisionHitmark) + } + + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{}, + highPlungeRadius), + highPlungeHitmark, + highPlungeHitmark, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(plungeFrames), + AnimationLength: plungeFrames[action.ActionJump], + CanQueueAfter: plungeFrames[action.ActionDash], + State: action.PlungeAttackState, + }, nil +} + +func (c *char) HighPlungeAttack(p map[string]int) (action.Info, error) { + defer func() { + c.Core.Player.SetAirborne(player.Grounded) + c.jmpSrc = 0 + }() + + if c.Core.Player.Airborne() != player.AirborneOroron { + return c.Character.HighPlungeAttack(p) + } + + if p["fall"] != 0 { + return c.fall(), nil + } + + return c.HighPlungeAirborneOroron(p) +} diff --git a/internal/template/nightsoul/nightsoul.go b/internal/template/nightsoul/nightsoul.go index 055148d0b8..3de6c3a566 100644 --- a/internal/template/nightsoul/nightsoul.go +++ b/internal/template/nightsoul/nightsoul.go @@ -35,11 +35,13 @@ func (s *State) EnterBlessing(amount float64) { Write("points", s.nightsoulPoints) } +// Only exits regular NS blessing, not special transmission blessing. func (s *State) ExitBlessing() { s.char.DeleteStatus(NightsoulBlessingStatus) s.c.Log.NewEvent("exit nightsoul blessing", glog.LogCharacterEvent, s.char.Index) } +// Returns true if either normal or transmission blessing is active. func (s *State) HasBlessing() bool { return s.char.StatusIsActive(NightsoulBlessingStatus) } diff --git a/pkg/core/player/player.go b/pkg/core/player/player.go index ef53b192a5..3ac2f6fb0f 100644 --- a/pkg/core/player/player.go +++ b/pkg/core/player/player.go @@ -306,6 +306,7 @@ const ( AirborneVenti AirborneKazuha AirborneXianyun + AirborneOroron TerminateAirborne ) diff --git a/ui/packages/docs/src/components/Actions/character_data.json b/ui/packages/docs/src/components/Actions/character_data.json index 95766e91bc..67f72f75a0 100644 --- a/ui/packages/docs/src/components/Actions/character_data.json +++ b/ui/packages/docs/src/components/Actions/character_data.json @@ -2138,7 +2138,7 @@ }, { "ability": "aim", - "legal": true + "notes": "Mid-air aim not implemented." }, { "ability": "skill", @@ -2163,6 +2163,10 @@ { "ability": "swap", "legal": true + }, + { + "ability": "high_plunge", + "notes": "Previous action must be an Ororon High Jump. Plunge from Xianyun's burst is not implemented." } ], "qiqi": [ diff --git a/ui/packages/docs/src/components/Params/character_data.json b/ui/packages/docs/src/components/Params/character_data.json index 5088dfe983..eda083ba09 100644 --- a/ui/packages/docs/src/components/Params/character_data.json +++ b/ui/packages/docs/src/components/Params/character_data.json @@ -1096,6 +1096,21 @@ "ability": "skill", "param": "travel", "desc": "Travel time of bounce between targets in frames. Default: 10." + }, + { + "ability": "skill", + "param": "hold", + "desc": "0 for a normal jump (default), nonzero for an extended leap as buffed from his Night Realm's Gift. Airborne Frames = hold-1. -1 for a max-duration jump. Grants status AirborneOroron. A high_plunge action with fall=true will be called automatically if another action is not taken within Ororon's jump duration." + }, + { + "ability": "high_plunge", + "param": "collision", + "desc": "0 for no collision dmg (default), 1 for collision dmg." + }, + { + "ability": "high_plunge", + "param": "fall", + "desc": "0 for a normal high_plunge, 1 to simulate a damageless release from high jump. After fall has begun, high_plunge cannot be used until Ororon is airborne again." } ], "qiqi": [