diff --git a/go.mod b/go.mod index 1438bb970e..3e55cc4301 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/alvaroloes/enumer v1.1.2 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -86,6 +87,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.9 // indirect + github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index 33f0eab558..07dcf2d417 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= +github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY= +github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= @@ -230,6 +232,8 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= @@ -243,6 +247,8 @@ github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sostronk/enumer v0.0.0-20210216185328-9ca19f4277f2 h1:s9WPmKKFORe/e5W5BvWTEOznLJF7uIgp+xfVcZIVjQc= +github.com/sostronk/enumer v0.0.0-20210216185328-9ca19f4277f2/go.mod h1:F5jPVK0JGhLxpXEzG9c7PIl0Ztvhg2tScHcOlBStzUY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -375,6 +381,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -417,9 +424,11 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= diff --git a/pkg/core/attacks/attack.go b/pkg/core/attacks/attack.go index 520fb6a635..cb83cba48f 100644 --- a/pkg/core/attacks/attack.go +++ b/pkg/core/attacks/attack.go @@ -1,5 +1,6 @@ package attacks +//go:generate enumer -type=AttackTag type AttackTag int // attacktag is used instead of actions etc.. const ( diff --git a/pkg/core/attacks/attacktag_enumer.go b/pkg/core/attacks/attacktag_enumer.go new file mode 100644 index 0000000000..8572d22f08 --- /dev/null +++ b/pkg/core/attacks/attacktag_enumer.go @@ -0,0 +1,72 @@ +// Code generated by "enumer -type=AttackTag"; DO NOT EDIT. + +package attacks + +import ( + "fmt" +) + +const _AttackTagName = "AttackTagNoneAttackTagNormalAttackTagExtraAttackTagPlungeAttackTagElementalArtAttackTagElementalArtHoldAttackTagElementalBurstAttackTagWeaponSkillAttackTagMonaBubbleBreakAttackTagNoneStatReactionAttackDelimAttackTagOverloadDamageAttackTagSuperconductDamageAttackTagECDamageAttackTagShatterAttackTagSwirlPyroAttackTagSwirlHydroAttackTagSwirlCryoAttackTagSwirlElectroAttackTagBurningDamageAttackTagBloomAttackTagBountifulCoreAttackTagBurgeonAttackTagHyperbloomAttackTagLength" + +var _AttackTagIndex = [...]uint16{0, 13, 28, 42, 57, 78, 103, 126, 146, 170, 187, 206, 229, 256, 273, 289, 307, 326, 344, 365, 387, 401, 423, 439, 458, 473} + +func (i AttackTag) String() string { + if i < 0 || i >= AttackTag(len(_AttackTagIndex)-1) { + return fmt.Sprintf("AttackTag(%d)", i) + } + return _AttackTagName[_AttackTagIndex[i]:_AttackTagIndex[i+1]] +} + +var _AttackTagValues = []AttackTag{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} + +var _AttackTagNameToValueMap = map[string]AttackTag{ + _AttackTagName[0:13]: 0, + _AttackTagName[13:28]: 1, + _AttackTagName[28:42]: 2, + _AttackTagName[42:57]: 3, + _AttackTagName[57:78]: 4, + _AttackTagName[78:103]: 5, + _AttackTagName[103:126]: 6, + _AttackTagName[126:146]: 7, + _AttackTagName[146:170]: 8, + _AttackTagName[170:187]: 9, + _AttackTagName[187:206]: 10, + _AttackTagName[206:229]: 11, + _AttackTagName[229:256]: 12, + _AttackTagName[256:273]: 13, + _AttackTagName[273:289]: 14, + _AttackTagName[289:307]: 15, + _AttackTagName[307:326]: 16, + _AttackTagName[326:344]: 17, + _AttackTagName[344:365]: 18, + _AttackTagName[365:387]: 19, + _AttackTagName[387:401]: 20, + _AttackTagName[401:423]: 21, + _AttackTagName[423:439]: 22, + _AttackTagName[439:458]: 23, + _AttackTagName[458:473]: 24, +} + +// AttackTagString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func AttackTagString(s string) (AttackTag, error) { + if val, ok := _AttackTagNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to AttackTag values", s) +} + +// AttackTagValues returns all values of the enum +func AttackTagValues() []AttackTag { + return _AttackTagValues +} + +// IsAAttackTag returns "true" if the value is listed in the enum definition. "false" otherwise +func (i AttackTag) IsAAttackTag() bool { + for _, v := range _AttackTagValues { + if i == v { + return true + } + } + return false +} diff --git a/pkg/core/event/event.go b/pkg/core/event/event.go index 017f807f19..e1d5d0a8c2 100644 --- a/pkg/core/event/event.go +++ b/pkg/core/event/event.go @@ -1,5 +1,6 @@ package event +//go:generate enumer -type=Event type Event int const ( diff --git a/pkg/core/event/event_enumer.go b/pkg/core/event/event_enumer.go new file mode 100644 index 0000000000..d4938ff1aa --- /dev/null +++ b/pkg/core/event/event_enumer.go @@ -0,0 +1,113 @@ +// Code generated by "enumer -type=Event"; DO NOT EDIT. + +package event + +import ( + "fmt" +) + +const _EventName = "OnEnemyHitOnPlayerHitOnGadgetHitOnEnemyDamageOnGadgetDamageOnApplyAttackOnAuraDurabilityAddedOnAuraDurabilityDepletedReactionEventStartDelimOnOverloadOnSuperconductOnMeltOnVaporizeOnFrozenOnElectroChargedOnSwirlHydroOnSwirlCryoOnSwirlElectroOnSwirlPyroOnCrystallizeHydroOnCrystallizeCryoOnCrystallizeElectroOnCrystallizePyroOnAggravateOnSpreadOnQuickenOnBloomOnHyperbloomOnBurgeonOnBurningOnShatterReactionEventEndDelimOnDendroCoreOnStamUseOnShieldedOnShieldBreakOnConstructSpawnedOnCharacterSwapOnParticleReceivedOnEnergyChangeOnTargetDiedOnTargetMovedOnCharacterHitOnCharacterHurtOnHPDebtOnHealOnPlayerPreHPDrainOnPlayerHPDrainOnNightsoulBurstOnNightsoulGenerateOnNightsoulConsumeOnActionFailedOnActionExecOnSkillOnBurstOnAttackOnChargeAttackOnPlungeOnAimShootOnDashOnInitializeOnStateChangeOnEnemyAddedOnTickOnSimEndedSuccessfullyEndEventTypes" + +var _EventIndex = [...]uint16{0, 10, 21, 32, 45, 59, 72, 93, 117, 140, 150, 164, 170, 180, 188, 204, 216, 227, 241, 252, 270, 287, 307, 324, 335, 343, 352, 359, 371, 380, 389, 398, 419, 431, 440, 450, 463, 481, 496, 514, 528, 540, 553, 567, 582, 590, 596, 614, 629, 645, 664, 682, 696, 708, 715, 722, 730, 744, 752, 762, 768, 780, 793, 805, 811, 833, 846} + +func (i Event) String() string { + if i < 0 || i >= Event(len(_EventIndex)-1) { + return fmt.Sprintf("Event(%d)", i) + } + return _EventName[_EventIndex[i]:_EventIndex[i+1]] +} + +var _EventValues = []Event{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65} + +var _EventNameToValueMap = map[string]Event{ + _EventName[0:10]: 0, + _EventName[10:21]: 1, + _EventName[21:32]: 2, + _EventName[32:45]: 3, + _EventName[45:59]: 4, + _EventName[59:72]: 5, + _EventName[72:93]: 6, + _EventName[93:117]: 7, + _EventName[117:140]: 8, + _EventName[140:150]: 9, + _EventName[150:164]: 10, + _EventName[164:170]: 11, + _EventName[170:180]: 12, + _EventName[180:188]: 13, + _EventName[188:204]: 14, + _EventName[204:216]: 15, + _EventName[216:227]: 16, + _EventName[227:241]: 17, + _EventName[241:252]: 18, + _EventName[252:270]: 19, + _EventName[270:287]: 20, + _EventName[287:307]: 21, + _EventName[307:324]: 22, + _EventName[324:335]: 23, + _EventName[335:343]: 24, + _EventName[343:352]: 25, + _EventName[352:359]: 26, + _EventName[359:371]: 27, + _EventName[371:380]: 28, + _EventName[380:389]: 29, + _EventName[389:398]: 30, + _EventName[398:419]: 31, + _EventName[419:431]: 32, + _EventName[431:440]: 33, + _EventName[440:450]: 34, + _EventName[450:463]: 35, + _EventName[463:481]: 36, + _EventName[481:496]: 37, + _EventName[496:514]: 38, + _EventName[514:528]: 39, + _EventName[528:540]: 40, + _EventName[540:553]: 41, + _EventName[553:567]: 42, + _EventName[567:582]: 43, + _EventName[582:590]: 44, + _EventName[590:596]: 45, + _EventName[596:614]: 46, + _EventName[614:629]: 47, + _EventName[629:645]: 48, + _EventName[645:664]: 49, + _EventName[664:682]: 50, + _EventName[682:696]: 51, + _EventName[696:708]: 52, + _EventName[708:715]: 53, + _EventName[715:722]: 54, + _EventName[722:730]: 55, + _EventName[730:744]: 56, + _EventName[744:752]: 57, + _EventName[752:762]: 58, + _EventName[762:768]: 59, + _EventName[768:780]: 60, + _EventName[780:793]: 61, + _EventName[793:805]: 62, + _EventName[805:811]: 63, + _EventName[811:833]: 64, + _EventName[833:846]: 65, +} + +// EventString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func EventString(s string) (Event, error) { + if val, ok := _EventNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Event values", s) +} + +// EventValues returns all values of the enum +func EventValues() []Event { + return _EventValues +} + +// IsAEvent returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Event) IsAEvent() bool { + for _, v := range _EventValues { + if i == v { + return true + } + } + return false +} diff --git a/pkg/gcs/sysfunc.go b/pkg/gcs/sysfunc.go index a0f3641525..f9a8496fd2 100644 --- a/pkg/gcs/sysfunc.go +++ b/pkg/gcs/sysfunc.go @@ -1,20 +1,15 @@ package gcs import ( - "errors" "fmt" "math" "strings" "github.com/genshinsim/gcsim/pkg/core/action" - "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/event" - "github.com/genshinsim/gcsim/pkg/core/geometry" "github.com/genshinsim/gcsim/pkg/core/glog" "github.com/genshinsim/gcsim/pkg/core/keys" "github.com/genshinsim/gcsim/pkg/gcs/ast" - "github.com/genshinsim/gcsim/pkg/reactable" - "github.com/genshinsim/gcsim/pkg/shortcut" ) func (e *Eval) initSysFuncs(env *Env) { @@ -38,6 +33,8 @@ func (e *Eval) initSysFuncs(env *Env) { e.addSysFunc("kill_target", e.killTarget, env) e.addSysFunc("is_target_dead", e.isTargetDead, env) e.addSysFunc("pick_up_crystallize", e.pickUpCrystallize, env) + e.addSysFunc("add_mod", e.addMod, env) + e.addSysFunc("heal", e.heal, env) // math e.addSysFunc("sin", e.sin, env) @@ -58,6 +55,48 @@ func (e *Eval) addSysFunc(name string, f func(c *ast.CallExpr, env *Env) (Obj, e env.varMap[name] = &obj } +// Get int if possible, raise error if not +func (e *Eval) getInt(callFunc string, argNum int, c *ast.CallExpr, env *Env) (int, error) { + // should eval to a number + val, err := e.evalExpr(c.Args[argNum], env) + if err != nil { + return 0, err + } + + n, ok := val.(*number) + if !ok { + return 0, fmt.Errorf("%v argument %v should evaluate to a number, got %v", callFunc, argNum+1, val.Inspect()) + } + + f := int(n.ival) + if n.isFloat { + f = int(n.fval) + } + + return f, nil +} + +// Get float if possible, raise error if not +func (e *Eval) getFloat(callFunc string, argNum int, c *ast.CallExpr, env *Env) (float64, error) { + // should eval to a number + val, err := e.evalExpr(c.Args[argNum], env) + if err != nil { + return 0.0, err + } + + n, ok := val.(*number) + if !ok { + return 0.0, fmt.Errorf("%v argument %v should evaluate to a number, got %v", callFunc, argNum+1, val.Inspect()) + } + + f := n.fval + if !n.isFloat { + f = float64(n.ival) + } + + return f, nil +} + func (e *Eval) print(c *ast.CallExpr, env *Env) (Obj, error) { // concat all args var sb strings.Builder @@ -212,322 +251,6 @@ func (e *Eval) typeval(c *ast.CallExpr, env *Env) (Obj, error) { return &strval{str}, nil } -func (e *Eval) setPlayerPos(c *ast.CallExpr, env *Env) (Obj, error) { - // set_player_pos(x, y) - if len(c.Args) != 2 { - return nil, fmt.Errorf("invalid number of params for set_player_pos, expected 2 got %v", len(c.Args)) - } - - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("set_player_pos argument x coord should evaluate to a number, got %v", t.Inspect()) - } - // n should be float - x := n.fval - if !n.isFloat { - x = float64(n.ival) - } - - t, err = e.evalExpr(c.Args[1], env) - if err != nil { - return nil, err - } - n, ok = t.(*number) - if !ok { - return nil, fmt.Errorf("set_player_pos argument y coord should evaluate to a number, got %v", t.Inspect()) - } - // n should be float - y := n.fval - if !n.isFloat { - y = float64(n.ival) - } - - e.Core.Combat.SetPlayerPos(geometry.Point{X: x, Y: y}) - e.Core.Combat.Player().SetDirectionToClosestEnemy() - - return bton(true), nil -} - -func (e *Eval) setParticleDelay(c *ast.CallExpr, env *Env) (Obj, error) { - // set_particle_delay("character", x); - if len(c.Args) != 2 { - return nil, fmt.Errorf("invalid number of params for set_particle_delay, expected 2 got %v", len(c.Args)) - } - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - name, ok := t.(*strval) - if !ok { - return nil, fmt.Errorf("set_particle_delay first argument should evaluate to a string, got %v", t.Inspect()) - } - - // check name exists on team - ck, ok := shortcut.CharNameToKey[name.str] - if !ok { - return nil, fmt.Errorf("set_particle_delay first argument %v is not a valid character", name.str) - } - - char, ok := e.Core.Player.ByKey(ck) - if !ok { - return nil, fmt.Errorf("set_particle_delay: %v is not on this team", name.str) - } - - t, err = e.evalExpr(c.Args[1], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("set_particle_delay second argument should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - delay := int(n.ival) - if n.isFloat { - delay = int(n.fval) - } - if delay < 0 { - delay = 0 - } - - char.ParticleDelay = delay - - return &number{}, nil -} - -// Set SwapICD to any integer, to simulate booking. May be any non-negative integer. -func (e *Eval) setSwapICD(c *ast.CallExpr, env *Env) (Obj, error) { - // setSwapCD - - // set_player_pos(x, y) - if len(c.Args) != 1 { - return nil, fmt.Errorf("invalid number of params for set_swap_icd, expected 1 got %v", len(c.Args)) - } - - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("set_swap_icd argument x coord should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - x := int(n.ival) - if n.isFloat { - x = int(n.fval) - } - - if x < 0 { - return nil, fmt.Errorf("invald value for swap icd, expected non-negative integer, got %v", x) - } - - e.Core.Player.SetSwapICD(x) - return &null{}, nil -} - -func (e *Eval) setDefaultTarget(c *ast.CallExpr, env *Env) (Obj, error) { - if len(c.Args) != 1 { - return nil, fmt.Errorf("invalid number of params for set_default_target, expected 1 got %v", len(c.Args)) - } - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("set_default_target argument should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - idx := int(n.ival) - if n.isFloat { - idx = int(n.fval) - } - - // check if index is in range - if idx < 1 || idx > e.Core.Combat.EnemyCount() { - return nil, fmt.Errorf("index for set_default_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) - } - - e.Core.Combat.DefaultTarget = e.Core.Combat.Enemy(idx - 1).Key() - e.Core.Combat.Player().SetDirectionToClosestEnemy() - - return &null{}, nil -} - -func (e *Eval) setTargetPos(c *ast.CallExpr, env *Env) (Obj, error) { - // set_target_pos(1,x,y) - if len(c.Args) != 3 { - return nil, fmt.Errorf("invalid number of params for set_target_pos, expected 3 got %v", len(c.Args)) - } - - // all 3 param should eval to numbers - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("set_target_pos argument target index should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - idx := int(n.ival) - if n.isFloat { - idx = int(n.fval) - } - - t, err = e.evalExpr(c.Args[1], env) - if err != nil { - return nil, err - } - n, ok = t.(*number) - if !ok { - return nil, fmt.Errorf("set_target_pos argument x coord should evaluate to a number, got %v", t.Inspect()) - } - // n should be float - x := n.fval - if !n.isFloat { - x = float64(n.ival) - } - - t, err = e.evalExpr(c.Args[2], env) - if err != nil { - return nil, err - } - n, ok = t.(*number) - if !ok { - return nil, fmt.Errorf("set_target_pos argument y coord should evaluate to a number, got %v", t.Inspect()) - } - // n should be float - y := n.fval - if !n.isFloat { - y = float64(n.ival) - } - - // check if index is in range - if idx < 1 || idx > e.Core.Combat.EnemyCount() { - return nil, fmt.Errorf("index for set_default_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) - } - - e.Core.Combat.SetEnemyPos(idx-1, geometry.Point{X: x, Y: y}) - e.Core.Combat.Player().SetDirectionToClosestEnemy() - - return &null{}, nil -} - -func (e *Eval) killTarget(c *ast.CallExpr, env *Env) (Obj, error) { - // kill_target(1) - if !e.Core.Combat.DamageMode { - return nil, errors.New("damage mode is not activated") - } - - if len(c.Args) != 1 { - return nil, fmt.Errorf("invalid number of params for kill_target, expected 1 got %v", len(c.Args)) - } - - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("kill_target argument target index should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - idx := int(n.ival) - if n.isFloat { - idx = int(n.fval) - } - - // check if index is in range - if idx < 1 || idx > e.Core.Combat.EnemyCount() { - return nil, fmt.Errorf("index for kill_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) - } - - e.Core.Combat.KillEnemy(idx - 1) - - return &null{}, nil -} - -func (e *Eval) isTargetDead(c *ast.CallExpr, env *Env) (Obj, error) { - // is_target_dead(1) - if !e.Core.Combat.DamageMode { - return nil, errors.New("damage mode is not activated") - } - - if len(c.Args) != 1 { - return nil, fmt.Errorf("invalid number of params for is_target_dead, expected 1 got %v", len(c.Args)) - } - - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - n, ok := t.(*number) - if !ok { - return nil, fmt.Errorf("is_target_dead argument target index should evaluate to a number, got %v", t.Inspect()) - } - // n should be int - idx := int(n.ival) - if n.isFloat { - idx = int(n.fval) - } - - // check if index is in range - if idx < 1 || idx > e.Core.Combat.EnemyCount() { - return nil, fmt.Errorf("index for is_target_dead is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) - } - - return bton(!e.Core.Combat.Enemies()[idx-1].IsAlive()), nil -} - -func (e *Eval) pickUpCrystallize(c *ast.CallExpr, env *Env) (Obj, error) { - // pick_up_crystallize("element"); - if len(c.Args) != 1 { - return nil, fmt.Errorf("invalid number of params for pick_up_crystallize, expected 1 got %v", len(c.Args)) - } - t, err := e.evalExpr(c.Args[0], env) - if err != nil { - return nil, err - } - name, ok := t.(*strval) - if !ok { - return nil, fmt.Errorf("pick_up_crystallize argument element should evaluate to a string, got %v", t.Inspect()) - } - - // check if element is vaild - pickupEle := attributes.StringToEle(name.str) - if pickupEle == attributes.UnknownElement && name.str != "any" { - return nil, fmt.Errorf("pick_up_crystallize argument element %v is not a valid element", name.str) - } - - var count int64 - for _, g := range e.Core.Combat.Gadgets() { - shard, ok := g.(*reactable.CrystallizeShard) - // skip if no shard - if !ok { - continue - } - // skip if shard not specified element - if pickupEle != attributes.UnknownElement && shard.Shield.Ele != pickupEle { - continue - } - // try to pick up shard and stop if succeeded - if shard.AddShieldKillShard() { - count = 1 - break - } - } - - return &number{ - ival: count, - }, nil -} - func (e *Eval) isEven(c *ast.CallExpr, env *Env) (Obj, error) { // is_even(number goes in here) if len(c.Args) != 1 { diff --git a/pkg/gcs/sysfunc_player_enemy.go b/pkg/gcs/sysfunc_player_enemy.go new file mode 100644 index 0000000000..739a429ada --- /dev/null +++ b/pkg/gcs/sysfunc_player_enemy.go @@ -0,0 +1,817 @@ +package gcs + +import ( + "errors" + "fmt" + "strings" + + "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/event" + "github.com/genshinsim/gcsim/pkg/core/geometry" + "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/gcs/ast" + "github.com/genshinsim/gcsim/pkg/modifier" + "github.com/genshinsim/gcsim/pkg/reactable" + "github.com/genshinsim/gcsim/pkg/shortcut" +) + +// heal(amount [, optional targetCharKey, optional srcCharKey]) +// amount: float, percent of max hp to heal. +// Optional targetCharKey: char to heal. -1 to heal all chars. Default -1 +// Optional useAbsHeal: Consider amount to be an absolute heal value, rather than percent of target's max hp. Default 0 +// Forbidden srcCharKey: char to emit healing from. -1 to attribute to no chars. Default -1 +// Note: Per current understanding, the abyss card "heal 30% on burst" does not count as originating from any character. +// +// Therefore, this heal will always be counted as coming from no character. +// +// If a src char is given, the heal will be improved by the src char's HB% stat +func (e *Eval) heal(c *ast.CallExpr, env *Env) (Obj, error) { + funcName := "heal" + reqArgs := 1 + + // Defaults + targetCharKey := -1 + targetCharIdx := -1 + srcCharIdx := -1 + healType := info.HealTypePercent + + argLen := len(c.Args) + if argLen < reqArgs { + return nil, fmt.Errorf("invalid number of params for %v, expected between 1 and 2, got %v", + funcName, + len(c.Args)) + } + + // Amount + argNum := 0 + amount, err := e.getFloat(funcName, argNum, c, env) + if err != nil { + return nil, err + } + + // -- Optionals -- + // targetCharKey, -- only apply to this char. -1 for all chars. + argNum += 1 + if argNum < argLen { + targetCharKey, err = e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + } + + // Optional useAbsHeal: Consider amount to be an absolute heal value, rather than percent of target's max hp. Default 0 + argNum += 1 + if argNum < argLen { + useAbsHeal, err := e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + if useAbsHeal != 0 { + healType = info.HealTypeAbsolute + } + } + + // End of args parsing + if argNum >= argLen { + return nil, fmt.Errorf( + "invalid number of args to %v, expecting at most %v, got %v", + funcName, argNum+1, argLen, + ) + } + + foundTarget := targetCharKey == -1 + for _, char := range e.Core.Player.Chars() { + charKey := int(char.Base.Key) + if charKey == targetCharKey { + targetCharIdx = char.Index + foundTarget = true + } + } + if !foundTarget { + return nil, fmt.Errorf("charecter key passed to %v represents an absent character, %v", funcName, targetCharKey) + } + + e.Core.Player.Heal(info.HealInfo{ + Caller: srcCharIdx, + Target: targetCharIdx, + Type: healType, + Message: "SysFunc Heal", + Src: amount, + }) + + return &number{}, nil +} + +// add_mod( +// ------ Required +// modType, -- Type of mod, string +// key, -- Will overwrite existing mod instead of creating new mod if a key is reused, string +// amount, -- Numeric eval. Ignored for mod type "Status" +// ----- Optional +// statType, --Type of stat, string. Required for AttackMod or StatMod, otherwise ignored. +// AttackTag, -- Type of attack, string. Required for AttackMod and ReactBonusMod, otherwise ignored. +// Action, -- Type of action, key, Use .action. to retrieve proper key name. -1 for all actions. Default -1. +// charKey, -- only apply to this char. -1 for all chars. Use .keys. fields to get key. +// duration, -- frames to apply mod for. -1 for infinite. Default -1. +// hitlag, -- apply hitlag extension. Default 1 +// active, -- Mod only applies if on-field, 0 otherwise. Default 0. +// triggerEvent, -- advanced, type of Event from package `event` to trigger on. Default no trigger, apply immediately +// removalEvent, -- advanced, type of Event from package `event` to remove on. Default no trigger, apply immediately +// ) +// If no trigger is given +func (e *Eval) addMod(c *ast.CallExpr, env *Env) (Obj, error) { + funcName := "add_mod" + reqArgs := 3 + + // Defaults + statType := attributes.NoStat + attackTag := attacks.AttackTagNone + actionKey := action.Action(-1) + charKey := -1 + duration := -1 + hitlag := 1 + active := 0 + triggerEvent := event.EndEventTypes + removalEvent := event.EndEventTypes + + argLen := len(c.Args) + if argLen < reqArgs { + return nil, fmt.Errorf("invalid number of params for add_mod, expected between 2 and 10, got %v", len(c.Args)) + } + + // ModType + argNum := 0 + var sb strings.Builder + val, err := e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + modType := sb.String() + + // Key + argNum += 1 + sb = strings.Builder{} + val, err = e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + key := sb.String() + + // Amount + argNum += 1 + amount, err := e.getFloat(funcName, argNum, c, env) + if err != nil { + return nil, err + } + + // -- Optionals -- + // statType, --Type of stat, string. Required for AttackMod or StatMod. + argNum += 1 + if argNum < argLen { + sb = strings.Builder{} + val, err = e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + statTypeStr := sb.String() + statType = attributes.StrToStatType(statTypeStr) + } + + // AttackTag, -- Only apply mod to this action type. Required for AttackMod. + argNum += 1 + if argNum < argLen { + sb = strings.Builder{} + val, err = e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + attackTagStr := sb.String() + attackTag, err = attacks.AttackTagString(attackTagStr) + if err != nil { // Only matters if trying to add attack mod + attackTag = -1 + } + } + + // Action, -- Type of action, key, Use .action. to retrieve proper key name. -1 for all actions. Default -1. + argNum += 1 + if argNum < argLen { + actionKeyInt, err := e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + actionKey = action.Action(actionKeyInt) + } + + // charKey, -- only apply to this char. -1 for all chars. + argNum += 1 + if argNum < argLen { + charKey, err = e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + } + + // duration, -- frames to apply mod for. -1 for infinite. Default -1. + argNum += 1 + if argNum < argLen { + duration, err = e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + } + + // hitlag, -- apply hitlag extension. Default 1 + argNum += 1 + if argNum < argLen { + hitlag, err = e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + } + + // active, -- Mod only applies if on-field, 0 otherwise. Default 0. + argNum += 1 + if argNum < argLen { + active, err = e.getInt(funcName, argNum, c, env) + if err != nil { + return nil, err + } + } + + // triggerEvent, -- advanced, type of Event from package `event` to trigger on. Default no trigger, apply immediately + argNum += 1 + if argNum < argLen { + sb = strings.Builder{} + val, err = e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + triggerEventStr := sb.String() + triggerEvent, err = event.EventString(triggerEventStr) + if err != nil { + return nil, fmt.Errorf( + "invalid trigger event, got %v. For the full list of valid events, see documentation", + triggerEventStr, + ) + } + } + + // removalEvent, -- advanced, type of Event from package `event` to remove on. Default no trigger, apply immediately + argNum += 1 + if argNum < argLen { + sb = strings.Builder{} + val, err = e.evalExpr(c.Args[argNum], env) + if err != nil { + return nil, err + } + sb.WriteString(val.Inspect()) + removalEventStr := sb.String() + removalEvent, err = event.EventString(removalEventStr) + if err != nil { + return nil, fmt.Errorf( + "invalid removal event, got %v. For the full list of valid events, see documentation", + removalEventStr, + ) + } + if removalEvent == triggerEvent { + return nil, fmt.Errorf( + "trigger event and removal event cannot be the same, got %v (key: %v)", + removalEventStr, + removalEvent, + ) + } + } + + // End of args parsing + if argNum >= argLen { + return nil, fmt.Errorf( + "invalid number of args to %v, expecting at most %v, got %v", + funcName, argNum+1, argLen, + ) + } + + newMod := modifier.NewBaseWithHitlag + if hitlag == 0 { + newMod = modifier.NewBase + } + + foundChar := false + for _, char := range e.Core.Player.Chars() { + if charKey != -1 && charKey != int(char.Base.Key) { + break + } + foundChar = true + + activeOrAllCheck := func() bool { + if active != 0 { + if char.Index != e.Core.Player.Active() { + return false + } + } + return true + } + + modBase := newMod(key, duration) + addMod := func() {} + deleteMod := func() {} + switch modType { + case "Status": + addMod = func() { + char.AddStatus(key, duration, hitlag != 0) + } + deleteMod = func() { + char.DeleteStatus(key) + } + + case "AttackMod": + if statType == attributes.NoStat { + return nil, fmt.Errorf( + "invalid stat type for %v, got %v", funcName, statType.String()) + } + if attackTag == -1 { + return nil, fmt.Errorf( + "invalid attackTag type for %v, got %v", funcName, attackTag.String()) + } + + val := make([]float64, attributes.EndStatType) + val[statType] = amount + addMod = func() { + char.AddAttackMod(character.AttackMod{ + Base: modBase, + Amount: func(atk *combat.AttackEvent, t combat.Target) ([]float64, bool) { + if attackTag != attacks.AttackTagNone { + if atk.Info.AttackTag != attackTag { + return nil, false + } + } + if !activeOrAllCheck() { + return nil, false + } + return val, true + }, + }) + } + deleteMod = func() { + char.DeleteAttackMod(key) + } + + case "CooldownMod": + addMod = func() { + char.AddCooldownMod(character.CooldownMod{ + Base: modBase, + Amount: func(a action.Action) float64 { + if !activeOrAllCheck() { + return 0 + } + if actionKey != action.Action(-1) { + if a != actionKey { + return 0 + } + } + return amount + }, + }) + } + deleteMod = func() { + char.DeleteCooldownMod(key) + } + + case "DamageReductionMod": + addMod = func() { + char.AddDamageReductionMod(character.DamageReductionMod{ + Base: modBase, + Amount: func() (float64, bool) { + if !activeOrAllCheck() { + return 0, false + } + + return amount, false + }, + }) + } + deleteMod = func() { + char.DeleteDamageReductionMod(key) + } + + case "HealBonusMod": + addMod = func() { + char.AddHealBonusMod(character.HealBonusMod{ + Base: modBase, + Amount: func() (float64, bool) { + if !activeOrAllCheck() { + return 0, false + } + + return amount, false + }, + }) + } + deleteMod = func() { + char.DeleteHealBonusMod(key) + } + + case "ReactBonusMod": + if attackTag == -1 { + return nil, fmt.Errorf( + "invalid attackTag type for %v, got %v", funcName, attackTag.String()) + } + + addMod = func() { + char.AddReactBonusMod(character.ReactBonusMod{ + Base: modBase, + Amount: func(ai combat.AttackInfo) (float64, bool) { + if !activeOrAllCheck() { + return 0, false + } + + if attackTag != attacks.AttackTagNone { + if ai.AttackTag != attackTag { + return 0, false + } + } + return amount, true + }, + }) + } + deleteMod = func() { + char.DeleteReactBonusMod(key) + } + + case "StatMod": + if statType == attributes.NoStat || statType == -1 { + return nil, fmt.Errorf( + "invalid stat type for %v, got %v", funcName, statType.String()) + } + + val := make([]float64, attributes.EndStatType) + val[statType] = amount + addMod = func() { + char.AddStatMod(character.StatMod{ + Base: modBase, + Amount: func() ([]float64, bool) { + if !activeOrAllCheck() { + return nil, false + } + return val, true + }, + }) + } + deleteMod = func() { + char.DeleteStatMod(key) + } + } + + eventKey := key + char.Base.Key.String() + if triggerEvent != event.EndEventTypes { + e.Core.Events.Subscribe( + triggerEvent, + func(args ...interface{}) bool { + addMod() + return false + }, + eventKey) + } else { + addMod() + } + + if removalEvent != event.EndEventTypes { + e.Core.Events.Subscribe( + removalEvent, + func(args ...interface{}) bool { + deleteMod() + return false + }, + eventKey) + } + } + if !foundChar { + return nil, fmt.Errorf("charecter key passed to %v represents an absent character, %v", funcName, charKey) + } + + return &number{}, nil +} + +func (e *Eval) setTargetPos(c *ast.CallExpr, env *Env) (Obj, error) { + // set_target_pos(1,x,y) + if len(c.Args) != 3 { + return nil, fmt.Errorf("invalid number of params for set_target_pos, expected 3 got %v", len(c.Args)) + } + + // all 3 param should eval to numbers + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("set_target_pos argument target index should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + idx := int(n.ival) + if n.isFloat { + idx = int(n.fval) + } + + t, err = e.evalExpr(c.Args[1], env) + if err != nil { + return nil, err + } + n, ok = t.(*number) + if !ok { + return nil, fmt.Errorf("set_target_pos argument x coord should evaluate to a number, got %v", t.Inspect()) + } + // n should be float + x := n.fval + if !n.isFloat { + x = float64(n.ival) + } + + t, err = e.evalExpr(c.Args[2], env) + if err != nil { + return nil, err + } + n, ok = t.(*number) + if !ok { + return nil, fmt.Errorf("set_target_pos argument y coord should evaluate to a number, got %v", t.Inspect()) + } + // n should be float + y := n.fval + if !n.isFloat { + y = float64(n.ival) + } + + // check if index is in range + if idx < 1 || idx > e.Core.Combat.EnemyCount() { + return nil, fmt.Errorf("index for set_default_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) + } + + e.Core.Combat.SetEnemyPos(idx-1, geometry.Point{X: x, Y: y}) + e.Core.Combat.Player().SetDirectionToClosestEnemy() + + return &null{}, nil +} + +func (e *Eval) killTarget(c *ast.CallExpr, env *Env) (Obj, error) { + // kill_target(1) + if !e.Core.Combat.DamageMode { + return nil, errors.New("damage mode is not activated") + } + + if len(c.Args) != 1 { + return nil, fmt.Errorf("invalid number of params for kill_target, expected 1 got %v", len(c.Args)) + } + + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("kill_target argument target index should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + idx := int(n.ival) + if n.isFloat { + idx = int(n.fval) + } + + // check if index is in range + if idx < 1 || idx > e.Core.Combat.EnemyCount() { + return nil, fmt.Errorf("index for kill_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) + } + + e.Core.Combat.KillEnemy(idx - 1) + + return &null{}, nil +} + +func (e *Eval) isTargetDead(c *ast.CallExpr, env *Env) (Obj, error) { + // is_target_dead(1) + if !e.Core.Combat.DamageMode { + return nil, errors.New("damage mode is not activated") + } + + if len(c.Args) != 1 { + return nil, fmt.Errorf("invalid number of params for is_target_dead, expected 1 got %v", len(c.Args)) + } + + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("is_target_dead argument target index should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + idx := int(n.ival) + if n.isFloat { + idx = int(n.fval) + } + + // check if index is in range + if idx < 1 || idx > e.Core.Combat.EnemyCount() { + return nil, fmt.Errorf("index for is_target_dead is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) + } + + return bton(!e.Core.Combat.Enemies()[idx-1].IsAlive()), nil +} + +func (e *Eval) pickUpCrystallize(c *ast.CallExpr, env *Env) (Obj, error) { + // pick_up_crystallize("element"); + if len(c.Args) != 1 { + return nil, fmt.Errorf("invalid number of params for pick_up_crystallize, expected 1 got %v", len(c.Args)) + } + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + name, ok := t.(*strval) + if !ok { + return nil, fmt.Errorf("pick_up_crystallize argument element should evaluate to a string, got %v", t.Inspect()) + } + + // check if element is vaild + pickupEle := attributes.StringToEle(name.str) + if pickupEle == attributes.UnknownElement && name.str != "any" { + return nil, fmt.Errorf("pick_up_crystallize argument element %v is not a valid element", name.str) + } + + var count int64 + for _, g := range e.Core.Combat.Gadgets() { + shard, ok := g.(*reactable.CrystallizeShard) + // skip if no shard + if !ok { + continue + } + // skip if shard not specified element + if pickupEle != attributes.UnknownElement && shard.Shield.Ele != pickupEle { + continue + } + // try to pick up shard and stop if succeeded + if shard.AddShieldKillShard() { + count = 1 + break + } + } + + return &number{ + ival: count, + }, nil +} + +func (e *Eval) setPlayerPos(c *ast.CallExpr, env *Env) (Obj, error) { + // set_player_pos(x, y) + if len(c.Args) != 2 { + return nil, fmt.Errorf("invalid number of params for set_player_pos, expected 2 got %v", len(c.Args)) + } + + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("set_player_pos argument x coord should evaluate to a number, got %v", t.Inspect()) + } + // n should be float + x := n.fval + if !n.isFloat { + x = float64(n.ival) + } + + t, err = e.evalExpr(c.Args[1], env) + if err != nil { + return nil, err + } + n, ok = t.(*number) + if !ok { + return nil, fmt.Errorf("set_player_pos argument y coord should evaluate to a number, got %v", t.Inspect()) + } + // n should be float + y := n.fval + if !n.isFloat { + y = float64(n.ival) + } + + e.Core.Combat.SetPlayerPos(geometry.Point{X: x, Y: y}) + e.Core.Combat.Player().SetDirectionToClosestEnemy() + + return bton(true), nil +} + +func (e *Eval) setParticleDelay(c *ast.CallExpr, env *Env) (Obj, error) { + // set_particle_delay("character", x); + if len(c.Args) != 2 { + return nil, fmt.Errorf("invalid number of params for set_particle_delay, expected 2 got %v", len(c.Args)) + } + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + name, ok := t.(*strval) + if !ok { + return nil, fmt.Errorf("set_particle_delay first argument should evaluate to a string, got %v", t.Inspect()) + } + + // check name exists on team + ck, ok := shortcut.CharNameToKey[name.str] + if !ok { + return nil, fmt.Errorf("set_particle_delay first argument %v is not a valid character", name.str) + } + + char, ok := e.Core.Player.ByKey(ck) + if !ok { + return nil, fmt.Errorf("set_particle_delay: %v is not on this team", name.str) + } + + t, err = e.evalExpr(c.Args[1], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("set_particle_delay second argument should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + delay := int(n.ival) + if n.isFloat { + delay = int(n.fval) + } + if delay < 0 { + delay = 0 + } + + char.ParticleDelay = delay + + return &number{}, nil +} + +// Set SwapICD to any integer, to simulate booking. May be any non-negative integer. +func (e *Eval) setSwapICD(c *ast.CallExpr, env *Env) (Obj, error) { + // setSwapCD + + // set_player_pos(x, y) + if len(c.Args) != 1 { + return nil, fmt.Errorf("invalid number of params for set_swap_icd, expected 1 got %v", len(c.Args)) + } + + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("set_swap_icd argument x coord should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + x := int(n.ival) + if n.isFloat { + x = int(n.fval) + } + + if x < 0 { + return nil, fmt.Errorf("invald value for swap icd, expected non-negative integer, got %v", x) + } + + e.Core.Player.SetSwapICD(x) + return &null{}, nil +} + +func (e *Eval) setDefaultTarget(c *ast.CallExpr, env *Env) (Obj, error) { + if len(c.Args) != 1 { + return nil, fmt.Errorf("invalid number of params for set_default_target, expected 1 got %v", len(c.Args)) + } + t, err := e.evalExpr(c.Args[0], env) + if err != nil { + return nil, err + } + n, ok := t.(*number) + if !ok { + return nil, fmt.Errorf("set_default_target argument should evaluate to a number, got %v", t.Inspect()) + } + // n should be int + idx := int(n.ival) + if n.isFloat { + idx = int(n.fval) + } + + // check if index is in range + if idx < 1 || idx > e.Core.Combat.EnemyCount() { + return nil, fmt.Errorf("index for set_default_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx) + } + + e.Core.Combat.DefaultTarget = e.Core.Combat.Enemy(idx - 1).Key() + e.Core.Combat.Player().SetDirectionToClosestEnemy() + + return &null{}, nil +} diff --git a/pkg/stats/heal/heal.go b/pkg/stats/heal/heal.go index 63423190a4..3630021a71 100644 --- a/pkg/stats/heal/heal.go +++ b/pkg/stats/heal/heal.go @@ -19,12 +19,18 @@ type buffer struct { } func NewStat(core *core.Core) (stats.Collector, error) { + partySize := len(core.Player.Chars()) out := buffer{ - events: make([][]stats.HealEvent, len(core.Player.Chars())), + events: make([][]stats.HealEvent, partySize), } core.Events.Subscribe(event.OnHeal, func(args ...interface{}) bool { info := args[0].(*info.HealInfo) + // Abyss card buff- heal may originate from no character at all. Do not log. + if info.Caller < 0 || info.Caller >= partySize { + return false + } + target := args[1].(int) amount := args[2].(float64)