[Feature] Tera Rework (#5233)

* Commit old stashed changes

* Complete basic implementation of Tera

* Fix effectiveness test

* Make tera retain until forced recall or faint, regain on biome change

* Experimental sparkle fix

* Fix champion teras

* Attempted fix for double battles tera UI bug

* Fix the fix

* Fix linting and test issues

* Fix more tests

* Change int type

* Implement tera for ME trainers

* Cleanup species inclusivity check

* Make tera instant recharge if terapagos in party

* Make useless tera shards not generate

* Implement stellar tera damage boost

* Improve tera selection UI

* Tidy up animation and localisation

* Improve tera button sprite

* Fix Lance tera

* Make tera instant recharge during E4 in classic modes.

* Fix formatting in the tera common animation

The animation was also not playing due to `frameTimedEvents` being missing as well.

* Make tera effect start after animation

* Implement save migration

* Update version number for migration code

---------

Co-authored-by: Madmadness65 <blaze.the.fireman@gmail.com>
Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com>
This commit is contained in:
Xavion3 2025-02-17 08:20:50 +11:00 committed by GitHub
parent 4bc617bd5f
commit 90d32b886c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1784 additions and 433 deletions

View File

@ -0,0 +1,774 @@
{
"graphic": "terastallize",
"frames": [
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 0,
"opacity": 150,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 0,
"opacity": 225,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 70,
"zoomY": 70,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 70,
"zoomY": 70,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 90,
"zoomY": 90,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 90,
"zoomY": 90,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 200,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 100,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 100,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 60,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 60,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
},
{
"x": 0,
"y": -20,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 2,
"graphicFrame": 1,
"opacity": 60,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
100,
100,
100,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
255,
255,
255,
255
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
255,
255,
255,
0
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"tone": [
255,
255,
255,
255
],
"locked": true,
"priority": 1,
"focus": 2
},
{
"x": 128,
"y": -64,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 1,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 1
}
]
],
"frameTimedEvents": {},
"position": 4,
"hue": 0
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,158 @@
{ "frames": {
"unknown": {
"frame": { "x": 0, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"bug": {
"frame": { "x": 18, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"dark": {
"frame": { "x": 36, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"dragon": {
"frame": { "x": 54, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"electric": {
"frame": { "x": 72, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fairy": {
"frame": { "x": 0, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fighting": {
"frame": { "x": 18, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fire": {
"frame": { "x": 36, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"flying": {
"frame": { "x": 54, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ghost": {
"frame": { "x": 72, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"grass": {
"frame": { "x": 0, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ground": {
"frame": { "x": 18, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ice": {
"frame": { "x": 36, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"normal": {
"frame": { "x": 54, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"poison": {
"frame": { "x": 72, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"psychic": {
"frame": { "x": 0, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"rock": {
"frame": { "x": 18, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"steel": {
"frame": { "x": 36, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"water": {
"frame": { "x": 54, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"stellar": {
"frame": { "x": 72, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
}
},
"meta": {
"app": "https://www.aseprite.org/",
"version": "1.3.7-dev",
"image": "button_tera.png",
"format": "RGBA8888",
"size": { "w": 90, "h": 84 },
"scale": "1",
"frameTags": [
],
"layers": [
{ "name": "Sprite Sheet", "opacity": 255, "blendMode": "normal" }
],
"slices": [
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,158 @@
{ "frames": {
"unknown": {
"frame": { "x": 0, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"bug": {
"frame": { "x": 18, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"dark": {
"frame": { "x": 36, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"dragon": {
"frame": { "x": 54, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"electric": {
"frame": { "x": 72, "y": 0, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fairy": {
"frame": { "x": 0, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fighting": {
"frame": { "x": 18, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"fire": {
"frame": { "x": 36, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"flying": {
"frame": { "x": 54, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ghost": {
"frame": { "x": 72, "y": 21, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"grass": {
"frame": { "x": 0, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ground": {
"frame": { "x": 18, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"ice": {
"frame": { "x": 36, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"normal": {
"frame": { "x": 54, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"poison": {
"frame": { "x": 72, "y": 42, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"psychic": {
"frame": { "x": 0, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"rock": {
"frame": { "x": 18, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"steel": {
"frame": { "x": 36, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"water": {
"frame": { "x": 54, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
},
"stellar": {
"frame": { "x": 72, "y": 63, "w": 18, "h": 21 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 1, "y": 1, "w": 18, "h": 21 },
"sourceSize": { "w": 20, "h": 23 }
}
},
"meta": {
"app": "https://www.aseprite.org/",
"version": "1.3.7-dev",
"image": "button_tera.png",
"format": "RGBA8888",
"size": { "w": 90, "h": 84 },
"scale": "1",
"frameTags": [
],
"layers": [
{ "name": "Sprite Sheet", "opacity": 255, "blendMode": "normal" }
],
"slices": [
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -9,7 +9,7 @@ import type { Constructor } from "#app/utils";
import { isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "#app/utils";
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, ModifierBar, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier } from "./modifier/modifier";
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, ModifierBar, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier } from "./modifier/modifier";
import { PokeballType } from "#enums/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims";
import type { Phase } from "#app/phase";
@ -1373,7 +1373,11 @@ export default class BattleScene extends SceneBase {
for (const pokemon of this.getPlayerParty()) {
pokemon.resetBattleData();
pokemon.resetTera();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
if (pokemon.hasSpecies(Species.TERAPAGOS) || (this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190)) {
this.arena.playerTerasUsed = 0;
}
}
if (!this.trainer.visible) {
@ -1658,7 +1662,7 @@ export default class BattleScene extends SceneBase {
}
initPokemonSprite(sprite: Phaser.GameObjects.Sprite, pokemon?: Pokemon, hasShadow: boolean = false, ignoreOverride: boolean = false): Phaser.GameObjects.Sprite {
sprite.setPipeline(this.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: hasShadow, ignoreOverride: ignoreOverride, teraColor: pokemon ? getTypeRgb(pokemon.getTeraType()) : undefined });
sprite.setPipeline(this.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: hasShadow, ignoreOverride: ignoreOverride, teraColor: pokemon ? getTypeRgb(pokemon.getTeraType()) : undefined, isTerastallized: pokemon ? pokemon.isTerastallized : false });
this.spriteSparkleHandler.add(sprite);
return sprite;
}
@ -2589,11 +2593,8 @@ export default class BattleScene extends SceneBase {
const modifiersToRemove: PersistentModifier[] = [];
const modifierPromises: Promise<boolean>[] = [];
if (modifier instanceof PersistentModifier) {
if (modifier instanceof TerastallizeModifier) {
modifiersToRemove.push(...(this.findModifiers(m => m instanceof TerastallizeModifier && m.pokemonId === modifier.pokemonId)));
}
if ((modifier as PersistentModifier).add(this.modifiers, !!virtual)) {
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
if (modifier instanceof PokemonFormChangeItemModifier) {
const pokemon = this.getPokemonById(modifier.pokemonId);
if (pokemon) {
success = modifier.apply(pokemon, true);
@ -2670,11 +2671,8 @@ export default class BattleScene extends SceneBase {
addEnemyModifier(modifier: PersistentModifier, ignoreUpdate?: boolean, instant?: boolean): Promise<void> {
return new Promise(resolve => {
const modifiersToRemove: PersistentModifier[] = [];
if (modifier instanceof TerastallizeModifier) {
modifiersToRemove.push(...(this.findModifiers(m => m instanceof TerastallizeModifier && m.pokemonId === modifier.pokemonId, false)));
}
if ((modifier as PersistentModifier).add(this.enemyModifiers, false)) {
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
if (modifier instanceof PokemonFormChangeItemModifier) {
const pokemon = this.getPokemonById(modifier.pokemonId);
if (pokemon) {
modifier.apply(pokemon, true);
@ -2798,6 +2796,8 @@ export default class BattleScene extends SceneBase {
for (const modifier of modifiers) {
this.addEnemyModifier(modifier, true, true);
}
this.currentBattle.trainer.genAI(party);
}
party.forEach((enemyPokemon: EnemyPokemon, i: number) => {
@ -2929,7 +2929,7 @@ export default class BattleScene extends SceneBase {
const modifierIndex = modifiers.indexOf(modifier);
if (modifierIndex > -1) {
modifiers.splice(modifierIndex, 1);
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
if (modifier instanceof PokemonFormChangeItemModifier) {
const pokemon = this.getPokemonById(modifier.pokemonId);
if (pokemon) {
modifier.apply(pokemon, false);
@ -3130,7 +3130,8 @@ export default class BattleScene extends SceneBase {
name: p.name,
form: p.getFormKey(),
types: p.getTypes().map((type) => Type[type]),
teraType: p.getTeraType() !== Type.UNKNOWN ? Type[p.getTeraType()] : "",
teraType: Type[p.getTeraType()],
isTerastallized: p.isTerastallized,
level: p.level,
currentHP: p.hp,
maxHP: p.getMaxHp(),

View File

@ -92,6 +92,7 @@ export default class Battle {
public started: boolean = false;
public enemySwitchCounter: number = 0;
public turn: number = 0;
public preTurnCommands: TurnCommands;
public turnCommands: TurnCommands;
public playerParticipantIds: Set<number> = new Set<number>();
public battleScore: number = 0;
@ -180,6 +181,7 @@ export default class Battle {
incrementTurn(): void {
this.turn++;
this.turnCommands = Object.fromEntries(Utils.getEnumValues(BattlerIndex).map(bt => [ bt, null ]));
this.preTurnCommands = Object.fromEntries(Utils.getEnumValues(BattlerIndex).map(bt => [ bt, null ]));
this.battleSeedState = null;
}

View File

@ -239,37 +239,25 @@ export class PostBattleInitFormChangeAbAttr extends PostBattleInitAbAttr {
}
}
export class PostBattleInitStatStageChangeAbAttr extends PostBattleInitAbAttr {
export class PostTeraFormChangeStatChangeAbAttr extends AbAttr {
private stats: BattleStat[];
private stages: number;
private selfTarget: boolean;
constructor(stats: BattleStat[], stages: number, selfTarget?: boolean) {
constructor(stats: BattleStat[], stages: number) {
super();
this.stats = stats;
this.stages = stages;
this.selfTarget = !!selfTarget;
}
applyPostBattleInit(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder | null, args: any[]): boolean | Promise<boolean> {
const statStageChangePhases: StatStageChangePhase[] = [];
if (!simulated) {
if (this.selfTarget) {
statStageChangePhases.push(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages));
} else {
for (const opponent of pokemon.getOpponents()) {
statStageChangePhases.push(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages));
}
}
statStageChangePhases.push(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages));
for (const statStageChangePhase of statStageChangePhases) {
if (!this.selfTarget && !statStageChangePhase.getPokemon()?.summonData) {
globalScene.pushPhase(statStageChangePhase);
} else { // TODO: This causes the ability bar to be shown at the wrong time
globalScene.unshiftPhase(statStageChangePhase);
}
globalScene.unshiftPhase(statStageChangePhase);
}
}
@ -1307,7 +1295,7 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
if (
!pokemon.isTerastallized() &&
!pokemon.isTerastallized &&
move.id !== Moves.STRUGGLE &&
/**
* Skip moves that call other moves because these moves generate a following move that will trigger this ability attribute
@ -4788,7 +4776,7 @@ export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr {
}
override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, _args: any[]): boolean {
if (pokemon.isTerastallized()) {
if (pokemon.isTerastallized) {
return false;
}
const currentTerrain = globalScene.arena.getTerrainType();
@ -6201,7 +6189,7 @@ export function initAbilities() {
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
.condition((pokemon) => !pokemon.isTerastallized()),
.condition((pokemon) => !pokemon.isTerastallized),
new Ability(Abilities.QUICK_DRAW, 8)
.attr(BypassSpeedChanceAbAttr, 30),
new Ability(Abilities.UNSEEN_FIST, 8)
@ -6353,29 +6341,25 @@ export function initAbilities() {
new Ability(Abilities.TOXIC_CHAIN, 9)
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
new Ability(Abilities.EMBODY_ASPECT_TEAL, 9)
.attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPD ], 1, true)
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPD ], 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(), // Ogerpon tera interactions
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.EMBODY_ASPECT_WELLSPRING, 9)
.attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true)
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPDEF ], 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(), // Ogerpon tera interactions
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.EMBODY_ASPECT_HEARTHFLAME, 9)
.attr(PostBattleInitStatStageChangeAbAttr, [ Stat.ATK ], 1, true)
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.ATK ], 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(), // Ogerpon tera interactions
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.EMBODY_ASPECT_CORNERSTONE, 9)
.attr(PostBattleInitStatStageChangeAbAttr, [ Stat.DEF ], 1, true)
.attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.DEF ], 1)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.attr(NoTransformAbilityAbAttr)
.partial(), // Ogerpon tera interactions
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.TERA_SHIFT, 9)
.attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1)
.attr(UncopiableAbilityAbAttr)

View File

@ -56,6 +56,7 @@ export enum ChargeAnim {
export enum CommonAnim {
USE_ITEM = 2000,
HEALTH_UP,
TERASTALLIZE,
POISON = 2010,
TOXIC,
PARALYSIS,

View File

@ -2485,7 +2485,7 @@ export class TarShotTag extends BattlerTag {
* @returns whether the tag is applied
*/
override canAdd(pokemon: Pokemon): boolean {
return !pokemon.isTerastallized();
return !pokemon.isTerastallized;
}
override onAdd(pokemon: Pokemon): void {

View File

@ -791,7 +791,7 @@ export default class Move implements Localizable {
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, source, target, this, true, null, typeChangeMovePowerMultiplier);
const sourceTeraType = source.getTeraType();
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
power.value = 60;
}
@ -4634,7 +4634,7 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);
if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) >
if (user.isTerastallized && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) >
user.getEffectiveStat(Stat.SPATK, target, move, true, true, false, false, true)) {
category.value = MoveCategory.PHYSICAL;
return true;
@ -4662,7 +4662,7 @@ export class TeraBlastPowerAttr extends VariablePowerAttr {
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const power = args[0] as Utils.NumberHolder;
if (user.isTerastallized() && user.getTeraType() === Type.STELLAR) {
if (user.isTerastallized && user.getTeraType() === Type.STELLAR) {
power.value = 100;
return true;
}
@ -4720,30 +4720,6 @@ export class VariableMoveTypeAttr extends MoveAttr {
}
}
/**
* Attribute used for Tera Starstorm that changes the move type to Stellar
* @extends VariableMoveTypeAttr
*/
export class TeraStarstormTypeAttr extends VariableMoveTypeAttr {
/**
*
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move n/a
* @param args[0] {@linkcode Utils.NumberHolder} the move type
* @returns `true` if the move type is changed to {@linkcode Type.STELLAR}, `false` otherwise
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (user.isTerastallized() && (user.hasFusionSpecies(Species.TERAPAGOS) || user.species.speciesId === Species.TERAPAGOS)) {
const moveType = args[0] as Utils.NumberHolder;
moveType.value = Type.STELLAR;
return true;
}
return false;
}
}
export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveType = args[0];
@ -5009,7 +4985,7 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
return false;
}
if (user.isTerastallized()) {
if (user.isTerastallized) {
moveType.value = user.getTeraType(); // changes move type to tera type
return true;
}
@ -5018,6 +4994,30 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr {
}
}
/**
* Attribute used for Tera Starstorm that changes the move type to Stellar
* @extends VariableMoveTypeAttr
*/
export class TeraStarstormTypeAttr extends VariableMoveTypeAttr {
/**
*
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move n/a
* @param args[0] {@linkcode Utils.NumberHolder} the move type
* @returns `true` if the move type is changed to {@linkcode Type.STELLAR}, `false` otherwise
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (user.isTerastallized && user.hasSpecies(Species.TERAPAGOS)) {
const moveType = args[0] as Utils.NumberHolder;
moveType.value = Type.STELLAR;
return true;
}
return false;
}
}
export class MatchUserTypeAttr extends VariableMoveTypeAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveType = args[0];
@ -6345,7 +6345,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
return false;
}
if (user.isTerastallized() && user.getTeraType() === this.removedType) { // active tera types cannot be removed
if (user.isTerastallized && user.getTeraType() === this.removedType) { // active tera types cannot be removed
return false;
}
@ -6525,7 +6525,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
}
getCondition(): MoveConditionFunc {
return (user, target, move) => !target.isTerastallized() && !target.hasAbility(Abilities.MULTITYPE) && !target.hasAbility(Abilities.RKS_SYSTEM) && !(target.getTypes().length === 1 && target.getTypes()[0] === this.type);
return (user, target, move) => !target.isTerastallized && !target.hasAbility(Abilities.MULTITYPE) && !target.hasAbility(Abilities.RKS_SYSTEM) && !(target.getTypes().length === 1 && target.getTypes()[0] === this.type);
}
}
@ -6548,7 +6548,7 @@ export class AddTypeAttr extends MoveEffectAttr {
}
getCondition(): MoveConditionFunc {
return (user, target, move) => !target.isTerastallized() && !target.getTypes().includes(this.type);
return (user, target, move) => !target.isTerastallized && !target.getTypes().includes(this.type);
}
}
@ -11076,7 +11076,7 @@ export function initMoves() {
.attr(TeraMoveCategoryAttr)
.attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }),
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(Type.STELLAR) }),
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP)
.condition(failIfLastCondition),
@ -11271,7 +11271,7 @@ export function initMoves() {
new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(TeraMoveCategoryAttr)
.attr(TeraStarstormTypeAttr)
.attr(VariableTargetAttr, (user, target, move) => (user.hasFusionSpecies(Species.TERAPAGOS) || user.species.speciesId === Species.TERAPAGOS) && user.isTerastallized() ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER)
.attr(VariableTargetAttr, (user, target, move) => user.hasSpecies(Species.TERAPAGOS) && user.isTerastallized ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER)
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new AttackMove(Moves.FICKLE_BEAM, Type.DRAGON, MoveCategory.SPECIAL, 80, 100, 5, 30, 0, 9)
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc)

View File

@ -594,7 +594,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) });
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()), isTerastallized: tradedPokemon.isTerastallized });
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey());
sprite.setPipelineData("shiny", tradedPokemon.shiny);
@ -615,7 +615,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) });
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()), isTerastallized: tradedPokemon.isTerastallized });
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey());
sprite.setPipelineData("shiny", receivedPokemon.shiny);

View File

@ -1,5 +1,5 @@
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { generateModifierType, handleMysteryEncounterBattleFailed, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { handleMysteryEncounterBattleFailed, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs } from "#app/data/trainer-config";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene";
@ -23,7 +23,6 @@ import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/modifier/modifier-type";
import { Type } from "#enums/type";
import { getPokeballTintColor } from "#app/data/pokeball";
@ -387,11 +386,7 @@ function getPartyConfig(): EnemyPartyConfig {
nature: Nature.ADAMANT,
moveSet: [ Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH ],
ivs: [ 31, 31, 31, 31, 31, 31 ],
modifierConfigs: [
{
modifier: generateModifierType(modifierTypes.TERA_SHARD, [ Type.STEEL ]) as PokemonHeldItemModifierType,
}
]
tera: Type.STEEL,
}
]
};

View File

@ -46,6 +46,7 @@ import type { Variant } from "#app/data/variant";
import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Type } from "#app/enums/type";
/**
* Animates exclamation sprite over trainer's head at start of encounter
@ -98,6 +99,7 @@ export interface EnemyPokemonConfig {
modifierConfigs?: HeldModifierConfig[];
tags?: BattlerTagType[];
dataSource?: PokemonData;
tera?: Type;
aiType?: AiType;
}
@ -329,6 +331,14 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
tags.forEach(tag => enemyPokemon.addTag(tag));
}
// Set tera
if (config.tera && config.tera !== Type.UNKNOWN) {
enemyPokemon.teraType = config.tera;
if (battle.trainer) {
battle.trainer.config.setInstantTera(e);
}
}
// mysteryEncounterBattleEffects will only be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied
if (config.mysteryEncounterBattleEffects) {
enemyPokemon.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects;

View File

@ -61,7 +61,7 @@ export function doPokemonTransformationSequence(previousPokemon: PlayerPokemon,
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()) });
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()), isTerastallized: previousPokemon.isTerastallized });
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", previousPokemon.getSpriteKey());
sprite.setPipelineData("shiny", previousPokemon.shiny);

View File

@ -1,8 +1,7 @@
import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier";
import { PokemonFormChangeItemModifier } from "../modifier/modifier";
import type Pokemon from "../field/pokemon";
import { StatusEffect } from "#enums/status-effect";
import { MoveCategory, allMoves } from "./move";
import { Type } from "#enums/type";
import type { Constructor, nil } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
@ -399,23 +398,7 @@ export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger {
* @extends SpeciesFormChangeTrigger
*/
export class SpeciesFormChangeTeraTrigger extends SpeciesFormChangeTrigger {
/** The Tera type that triggers the form change */
private teraType: Type;
constructor(teraType: Type) {
super();
this.teraType = teraType;
this.description = i18next.t("pokemonEvolutions:Forms.tera", { teraType: i18next.t(`pokemonInfo:Type.${Type[this.teraType]}`) });
}
/**
* Checks if the associated Pokémon has the required Tera Shard that matches with the associated Tera type.
* @param {Pokemon} pokemon the Pokémon that is trying to do the form change
* @returns `true` if the Pokémon can change forms, `false` otherwise
*/
canChange(pokemon: Pokemon): boolean {
return !!globalScene.findModifier(m => m instanceof TerastallizeModifier && m.pokemonId === pokemon.id && m.teraType === this.teraType);
}
description = i18next.t("pokemonEvolutions:Forms.tera" );
}
/**
@ -425,10 +408,6 @@ export class SpeciesFormChangeTeraTrigger extends SpeciesFormChangeTrigger {
*/
export class SpeciesFormChangeLapseTeraTrigger extends SpeciesFormChangeTrigger {
description = i18next.t("pokemonEvolutions:Forms.teraLapse");
canChange(pokemon: Pokemon): boolean {
return !!globalScene.findModifier(m => m instanceof TerastallizeModifier && m.pokemonId === pokemon.id);
}
}
/**
@ -992,19 +971,19 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.OGERPON, "teal-mask", "wellspring-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.WELLSPRING_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "hearthflame-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.HEARTHFLAME_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "cornerstone-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.CORNERSTONE_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "teal-mask-tera", new SpeciesFormChangeTeraTrigger(Type.GRASS)),
new SpeciesFormChange(Species.OGERPON, "teal-mask-tera", "teal-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.GRASS)),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask", "wellspring-mask-tera", new SpeciesFormChangeTeraTrigger(Type.WATER)),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask-tera", "wellspring-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.WATER)),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask", "hearthflame-mask-tera", new SpeciesFormChangeTeraTrigger(Type.FIRE)),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask-tera", "hearthflame-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.FIRE)),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask", "cornerstone-mask-tera", new SpeciesFormChangeTeraTrigger(Type.ROCK)),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask-tera", "cornerstone-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.ROCK))
new SpeciesFormChange(Species.OGERPON, "teal-mask", "teal-mask-tera", new SpeciesFormChangeTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "teal-mask-tera", "teal-mask", new SpeciesFormChangeLapseTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask", "wellspring-mask-tera", new SpeciesFormChangeTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask-tera", "wellspring-mask", new SpeciesFormChangeLapseTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask", "hearthflame-mask-tera", new SpeciesFormChangeTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask-tera", "hearthflame-mask", new SpeciesFormChangeLapseTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask", "cornerstone-mask-tera", new SpeciesFormChangeTeraTrigger(), true),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask-tera", "cornerstone-mask", new SpeciesFormChangeLapseTeraTrigger(), true)
],
[Species.TERAPAGOS]: [
new SpeciesFormChange(Species.TERAPAGOS, "", "terastal", new SpeciesFormChangeAbilityTrigger(), true),
new SpeciesFormChange(Species.TERAPAGOS, "terastal", "stellar", new SpeciesFormChangeTeraTrigger(Type.STELLAR)),
new SpeciesFormChange(Species.TERAPAGOS, "stellar", "terastal", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.STELLAR))
new SpeciesFormChange(Species.TERAPAGOS, "terastal", "stellar", new SpeciesFormChangeTeraTrigger(), true),
new SpeciesFormChange(Species.TERAPAGOS, "stellar", "terastal", new SpeciesFormChangeLapseTeraTrigger(), true)
],
[Species.GALAR_DARMANITAN]: [
new SpeciesFormChange(Species.GALAR_DARMANITAN, "", "zen", new SpeciesFormChangeAbilityTrigger(), true),

View File

@ -175,11 +175,51 @@ export const trainerPartyTemplates = {
type PartyTemplateFunc = () => TrainerPartyTemplate;
type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon;
type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[];
type GenAIFunc = (party: EnemyPokemon[]) => void;
export interface PartyMemberFuncs {
[key: number]: PartyMemberFunc
}
export enum TeraAIMode {
NO_TERA,
INSTANT_TERA,
SMART_TERA
}
/**
* Stores data and helper functions about a trainers AI options.
*/
export class TrainerAI {
public teraMode: TeraAIMode = TeraAIMode.NO_TERA;
public instantTeras: number[];
/**
* @param canTerastallize Whether this trainer is allowed to tera
*/
constructor(teraMode: TeraAIMode = TeraAIMode.NO_TERA) {
this.teraMode = teraMode;
this.instantTeras = [];
}
/**
* Checks if a trainer can tera
* @returns Whether this trainer can currently tera
*/
public canTerastallize() {
return this.teraMode !== TeraAIMode.NO_TERA;
}
/**
* Sets a pokemon on this AI to just instantly tera on first move used
* @param index The index of the pokemon to instantly tera
*/
public setInstantTera(index: number) {
this.teraMode = TeraAIMode.INSTANT_TERA;
this.instantTeras.push(index);
}
}
export class TrainerConfig {
public trainerType: TrainerType;
public trainerTypeDouble: TrainerType;
@ -203,6 +243,7 @@ export class TrainerConfig {
public doubleEncounterBgm: string;
public victoryBgm: string;
public genModifiersFunc: GenModifiersFunc;
public genAIFuncs: GenAIFunc[] = [];
public modifierRewardFuncs: ModifierTypeFunc[] = [];
public partyTemplates: TrainerPartyTemplate[];
public partyTemplateFunc: PartyTemplateFunc;
@ -212,6 +253,7 @@ export class TrainerConfig {
public speciesFilter: PokemonSpeciesFilter;
public specialtyTypes: Type[] = [];
public hasVoucher: boolean = false;
public trainerAI: TrainerAI;
public encounterMessages: string[] = [];
public victoryMessages: string[] = [];
@ -227,6 +269,7 @@ export class TrainerConfig {
constructor(trainerType: TrainerType, allowLegendaries?: boolean) {
this.trainerType = trainerType;
this.trainerAI = new TrainerAI();
this.name = Utils.toReadableString(TrainerType[this.getDerivedType()]);
this.battleBgm = "battle_trainer";
this.mixedBattleBgm = "battle_trainer";
@ -550,6 +593,47 @@ export class TrainerConfig {
return this;
}
/**
* Sets random pokemon from the trainers team to instant tera. Uses their specialty types is they have one.
* @param count The amount of pokemon to have instant tera
* @returns this
*/
setRandomTeraModifiers(count: () => integer): TrainerConfig {
this.genAIFuncs.push((party: EnemyPokemon[]) => {
const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i);
for (let t = 0; t < Math.min(count(), party.length); t++) {
const randomIndex = Utils.randSeedItem(partyMemberIndexes);
partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1);
if (this.specialtyTypes?.length) {
party[randomIndex].teraType = Utils.randSeedItem(this.specialtyTypes);
}
this.trainerAI.setInstantTera(randomIndex);
}
});
return this;
}
/**
* Sets a specific pokemon to instant tera
* @param index The index within the team to have instant tera
* @returns this
*/
setInstantTera(index: number): TrainerConfig {
this.trainerAI.setInstantTera(index);
return this;
}
// function getRandomTeraModifiers(party: EnemyPokemon[], count: integer, types?: Type[]): PersistentModifier[] {
// const ret: PersistentModifier[] = [];
// const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i);
// for (let t = 0; t < Math.min(count, party.length); t++) {
// const randomIndex = Utils.randSeedItem(partyMemberIndexes);
// partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1);
// ret.push(modifierTypes.TERA_SHARD().generateType([], [ Utils.randSeedItem(types ? types : party[randomIndex].getTypes()) ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(party[randomIndex]) as PersistentModifier); // TODO: is the bang correct?
// }
// return ret;
// }
setEventModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig {
this.eventRewardFuncs = modifierTypeFuncs.map(func => () => {
const modifierTypeFunc = func();
@ -851,10 +935,7 @@ export class TrainerConfig {
this.setHasVoucher(true);
this.setBattleBgm("battle_unova_gym");
this.setVictoryBgm("victory_gym");
this.setGenModifiersFunc(party => {
const waveIndex = globalScene.currentBattle.waveIndex;
return getRandomTeraModifiers(party, waveIndex >= 100 ? 1 : 0, specialtyTypes.length ? specialtyTypes : undefined);
});
this.setRandomTeraModifiers(() => globalScene.currentBattle.waveIndex >= 100 ? 1 : 0);
return this;
}
@ -910,7 +991,7 @@ export class TrainerConfig {
this.setHasVoucher(true);
this.setBattleBgm("battle_unova_elite");
this.setVictoryBgm("victory_gym");
this.setGenModifiersFunc(party => getRandomTeraModifiers(party, 1, specialtyTypes.length ? specialtyTypes : undefined));
this.setRandomTeraModifiers(() => 1);
return this;
}
@ -1197,16 +1278,6 @@ function getSpeciesFilterRandomPartyMemberFunc(
};
}
function getRandomTeraModifiers(party: EnemyPokemon[], count: number, types?: Type[]): PersistentModifier[] {
const ret: PersistentModifier[] = [];
const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i);
for (let t = 0; t < Math.min(count, party.length); t++) {
const randomIndex = Utils.randSeedItem(partyMemberIndexes);
partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1);
ret.push(modifierTypes.TERA_SHARD().generateType([], [ Utils.randSeedItem(types ? types : party[randomIndex].getTypes()) ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(party[randomIndex]) as PersistentModifier); // TODO: is the bang correct?
}
return ret;
}
type SignatureSpecies = {
[key in string]: (Species | Species[])[];
@ -1747,10 +1818,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.gender = Gender.MALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.RED]: new TrainerConfig(++t).initForChampion(true).setBattleBgm("battle_johto_champion").setMixedBattleBgm("battle_johto_champion").setHasDouble("red_blue_double").setDoubleTrainerType(TrainerType.BLUE).setDoubleTitle("champion_double")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PIKACHU ], TrainerSlot.TRAINER, true, p => {
p.formIndex = 8; // G-Max Pikachu
@ -1774,10 +1842,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.gender = Gender.MALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.LANCE_CHAMPION]: new TrainerConfig(++t).setName("Lance").initForChampion(true).setBattleBgm("battle_johto_champion").setMixedBattleBgm("battle_johto_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GYARADOS, Species.KINGDRA ]))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.AERODACTYL ]))
@ -1787,16 +1852,15 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.CHARIZARD ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.TYRANITAR, Species.GARCHOMP, Species.KOMMO_O ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.TYRANITAR, Species.GARCHOMP, Species.KOMMO_O ], TrainerSlot.TRAINER, true, p => {
p.teraType = p.species.type1;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.DRAGONITE ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.STEVEN]: new TrainerConfig(++t).initForChampion(true).setBattleBgm("battle_hoenn_champion_g5").setMixedBattleBgm("battle_hoenn_champion_g6").setHasDouble("steven_wallace_double").setDoubleTrainerType(TrainerType.WALLACE).setDoubleTitle("champion_double")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SKARMORY ]))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.CRADILY, Species.ARMALDO ]))
@ -1814,10 +1878,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
p.generateName();
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.WALLACE]: new TrainerConfig(++t).initForChampion(true).setBattleBgm("battle_hoenn_champion_g5").setMixedBattleBgm("battle_hoenn_champion_g6").setHasDouble("wallace_steven_double").setDoubleTrainerType(TrainerType.STEVEN).setDoubleTitle("champion_double")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PELIPPER ], TrainerSlot.TRAINER, true, p => {
p.abilityIndex = 1; // Drizzle
@ -1840,10 +1901,7 @@ export const trainerConfigs: TrainerConfigs = {
p.gender = Gender.FEMALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.CYNTHIA]: new TrainerConfig(++t).initForChampion(false).setBattleBgm("battle_sinnoh_champion").setMixedBattleBgm("battle_sinnoh_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SPIRITOMB ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1853,7 +1911,9 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MILOTIC, Species.ROSERADE, Species.HISUI_ARCANINE ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MILOTIC, Species.ROSERADE, Species.HISUI_ARCANINE ], TrainerSlot.TRAINER, true, p => {
p.teraType = p.species.type1;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.TOGEKISS ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.setBoss(true, 2);
@ -1864,10 +1924,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.gender = Gender.FEMALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.ALDER]: new TrainerConfig(++t).initForChampion(true).setHasDouble("alder_iris_double").setDoubleTrainerType(TrainerType.IRIS).setDoubleTitle("champion_double").setBattleBgm("battle_champion_alder").setMixedBattleBgm("battle_champion_alder")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BOUFFALANT, Species.BRAVIARY ]))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HISUI_LILLIGANT, Species.HISUI_ZOROARK, Species.BASCULEGION ], TrainerSlot.TRAINER, true, p => {
@ -1882,16 +1939,15 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.CHANDELURE, Species.KROOKODILE, Species.REUNICLUS, Species.CONKELDURR ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.CHANDELURE, Species.KROOKODILE, Species.REUNICLUS, Species.CONKELDURR ], TrainerSlot.TRAINER, true, p => {
p.teraType = p.species.speciesId === Species.KROOKODILE ? Type.DARK : p.species.type1;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.VOLCARONA ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const pokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ pokemon.species.speciesId === Species.KROOKODILE ? pokemon.species.type2 : pokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(pokemon) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.IRIS]: new TrainerConfig(++t).initForChampion(false).setBattleBgm("battle_champion_iris").setMixedBattleBgm("battle_champion_iris").setHasDouble("iris_alder_double").setDoubleTrainerType(TrainerType.ALDER).setDoubleTitle("champion_double")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.DRUDDIGON ]))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ARCHEOPS ]))
@ -1899,7 +1955,9 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SALAMENCE, Species.HYDREIGON, Species.ARCHALUDON ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SALAMENCE, Species.HYDREIGON, Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => {
p.teraType = Type.DRAGON;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.LAPRAS ], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1; // G-Max Lapras
p.generateAndPopulateMoveset();
@ -1911,10 +1969,7 @@ export const trainerConfigs: TrainerConfigs = {
p.gender = Gender.FEMALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ Type.DRAGON ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.DIANTHA]: new TrainerConfig(++t).initForChampion(false).setMixedBattleBgm("battle_kalos_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.HAWLUCHA ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1927,6 +1982,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.TYRANTRUM, Species.AURORUS ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.abilityIndex = 2; // Rock Head Tyrantrum, Snow Warning Aurorus
p.teraType = p.species.speciesId === Species.TYRANTRUM ? Type.DRAGON : Type.ICE;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GOODRA ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1938,10 +1994,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.gender = Gender.FEMALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type2 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.KUKUI]: new TrainerConfig(++t).initForChampion(true).setMixedBattleBgm("battle_champion_kukui")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LYCANROC ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1965,11 +2018,9 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.INCINEROAR, Species.HISUI_DECIDUEYE ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
p.teraType = p.species.speciesId === Species.INCINEROAR ? Type.DARK : Type.FIGHTING;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[5];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type2 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(5),
[TrainerType.HAU]: new TrainerConfig(++t).initForChampion(true).setMixedBattleBgm("battle_alola_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ALOLA_RAICHU ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1982,6 +2033,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.TAPU_LELE, Species.TAPU_BULU ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.teraType = p.species.type1;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.ZYGARDE ], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1; // Zygarde 10% forme, Aura Break
@ -1993,10 +2045,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.gender = p.species.speciesId === Species.PRIMARINA ? Gender.FEMALE : Gender.MALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.LEON]: new TrainerConfig(++t).initForChampion(true).setMixedBattleBgm("battle_galar_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.AEGISLASH ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -2017,10 +2066,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.gender = Gender.MALE;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[3];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(3),
[TrainerType.MUSTARD]: new TrainerConfig(++t).initForChampion(true).setMixedBattleBgm("battle_mustard")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CORVIKNIGHT ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -2033,6 +2079,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.GALAR_SLOWBRO, Species.GALAR_SLOWKING ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.teraType = Type.PSYCHIC;
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALAR_DARMANITAN ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -2050,10 +2097,7 @@ export const trainerConfigs: TrainerConfigs = {
p.gender = Gender.MALE;
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[2];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type2 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(2),
[TrainerType.GEETA]: new TrainerConfig(++t).initForChampion(false).setMixedBattleBgm("battle_champion_geeta")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GLIMMORA ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -2070,11 +2114,9 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KINGAMBIT ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.abilityIndex = 1; // Supreme Overlord
p.teraType = Type.FLYING;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[5];
return [ modifierTypes.TERA_SHARD().generateType([], [ Type.FLYING ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(5),
[TrainerType.NEMONA]: new TrainerConfig(++t).initForChampion(false).setMixedBattleBgm("battle_champion_nemona")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LYCANROC ], TrainerSlot.TRAINER, true, p => {
p.formIndex = 0; // Midday form
@ -2086,16 +2128,15 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.MASTER_BALL;
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GHOLDENGO ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.ARMAROUGE, Species.CERULEDGE ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.ARMAROUGE, Species.CERULEDGE ], TrainerSlot.TRAINER, true, p => {
p.teraType = p.species.speciesId === Species.ARMAROUGE ? Type.PSYCHIC : Type.GHOST;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.gender = Gender.MALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type2 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.KIERAN]: new TrainerConfig(++t).initForChampion(true).setMixedBattleBgm("battle_champion_kieran")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.POLIWRATH, Species.POLITOED ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -2117,7 +2158,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.OGERPON ], TrainerSlot.TRAINER, true, p => {
p.formIndex = Utils.randSeedInt(4, 4); // Random Ogerpon Tera Mask
p.formIndex = Utils.randSeedInt(4); // Random Ogerpon Tera Mask
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
if (!p.moveset.some(move => !Utils.isNullOrUndefined(move) && move.moveId === Moves.IVY_CUDGEL)) { // Check if Ivy Cudgel is in the moveset, if not, replace the first move with Ivy Cudgel.
@ -2129,17 +2170,7 @@ export const trainerConfigs: TrainerConfigs = {
p.gender = Gender.MALE;
p.setBoss(true, 2);
}))
.setGenModifiersFunc(party => {
const starter = party[4];
let teraShardType: Type;
const pokemonType2 = starter.species.forms[starter.formIndex].type2;
if (starter.formIndex === 4 || Utils.isNullOrUndefined(pokemonType2)) {
teraShardType = starter.species.type1;
} else {
teraShardType = pokemonType2;
}
return [ modifierTypes.TERA_SHARD().generateType([], [ teraShardType ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(starter) as PersistentModifier ];
}),
.setInstantTera(4),
[TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL)
.setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE)
@ -2162,19 +2193,20 @@ export const trainerConfigs: TrainerConfigs = {
.setSpeciesFilter(species => species.baseTotal >= 540),
[TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
(p => p.abilityIndex = 0)))
(p => {
p.abilityIndex = 0;
p.teraType = p.species.type1;
})))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
.setSpeciesFilter(species => species.baseTotal >= 540)
.setGenModifiersFunc(party => {
const starter = party[0];
return [ modifierTypes.TERA_SHARD().generateType([], [ starter.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(starter) as PersistentModifier ]; // TODO: is the bang correct?
}),
.setInstantTera(0),
[TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
p => {
p.setBoss(true, 2);
p.abilityIndex = 0;
p.teraType = p.species.type1;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
@ -2185,15 +2217,13 @@ export const trainerConfigs: TrainerConfigs = {
p.shiny = true;
p.variant = 1;
}))
.setGenModifiersFunc(party => {
const starter = party[0];
return [ modifierTypes.TERA_SHARD().generateType([], [ starter.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(starter) as PersistentModifier ]; //TODO: is the bang correct?
}),
.setInstantTera(0),
[TrainerType.RIVAL_6]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(3).setEncounterBgm("final").setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_6)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
p => {
p.setBoss(true, 3);
p.abilityIndex = 0;
p.teraType = p.species.type1;
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true,
@ -2212,10 +2242,7 @@ export const trainerConfigs: TrainerConfigs = {
p.formIndex = 1; // Mega Rayquaza
p.generateName();
}))
.setGenModifiersFunc(party => {
const starter = party[0];
return [ modifierTypes.TERA_SHARD().generateType([], [ starter.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(starter) as PersistentModifier ]; // TODO: is the bang correct?
}),
.setInstantTera(0),
[TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig(t = TrainerType.ROCKET_BOSS_GIOVANNI_1).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", []).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN ], TrainerSlot.TRAINER, true, p => {
@ -2713,10 +2740,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.ULTRA_BALL;
p.generateName();
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[4];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
}),
.setInstantTera(4),
[TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
@ -2749,10 +2773,12 @@ export const trainerConfigs: TrainerConfigs = {
p.generateName();
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setGenModifiersFunc(party => {
const teraPokemon = party[0];
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
}),
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.ZAMAZENTA ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
}))
.setInstantTera(0),
[TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true)
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 3);

View File

@ -44,6 +44,7 @@ export class Arena {
public bgm: string;
public ignoreAbilities: boolean;
public ignoringEffectSource: BattlerIndex | null;
public playerTerasUsed: number;
/**
* Saves the number of times a party pokemon faints during a arena encounter.
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
@ -63,6 +64,7 @@ export class Arena {
this.bgm = bgm;
this.trainerPool = biomeTrainerPools[biome];
this.updatePoolsForTimeOfDay();
this.playerTerasUsed = 0;
this.playerFaints = playerFaints;
}

View File

@ -27,6 +27,9 @@ export default class PokemonSpriteSparkleHandler {
if (!s.visible || (s.parentContainer instanceof Pokemon && !s.parentContainer.parentContainer)) {
continue;
}
if (!(s.parentContainer instanceof Pokemon) || !(s.parentContainer as Pokemon).isTerastallized) {
continue;
}
const pokemon = s.parentContainer instanceof Pokemon ? s.parentContainer as Pokemon : null;
const parent = (pokemon || s).parentContainer;
const texture = s.texture;

View File

@ -52,7 +52,7 @@ import { getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
import { Type } from "#enums/type";
import { getLevelTotalExp } from "#app/data/exp";
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
import { PokeballType } from "#enums/pokeball";
import { Gender } from "#app/data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
@ -79,7 +79,7 @@ import { DexAttr } from "#app/system/game-data";
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities";
import { getNatureStatMultiplier } from "#app/data/nature";
import type { SpeciesFormChange } from "#app/data/pokemon-forms";
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms";
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms";
import { TerrainType } from "#app/data/terrain";
import type { TrainerSlot } from "#app/data/trainer-config";
import Overrides from "#app/overrides";
@ -163,6 +163,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public pokerus: boolean;
public switchOutStatus: boolean;
public evoCounter: number;
public teraType: Type;
public isTerastallized: boolean;
public stellarTypesBoosted: Type[];
public fusionSpecies: PokemonSpecies | null;
public fusionFormIndex: number;
@ -172,6 +175,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionGender: Gender;
public fusionLuck: number;
public fusionCustomPokemonData: CustomPokemonData | null;
public fusionTeraType: Type;
private summonDataPrimer: PokemonSummonData | null;
@ -269,8 +273,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck;
this.fusionCustomPokemonData = dataSource.fusionCustomPokemonData;
this.fusionTeraType = dataSource.fusionTeraType;
this.usedTMs = dataSource.usedTMs ?? [];
this.customPokemonData = new CustomPokemonData(dataSource.customPokemonData);
this.teraType = dataSource.teraType;
this.isTerastallized = dataSource.isTerastallized;
this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? [];
} else {
this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -319,6 +327,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.fusionLuck = this.luck;
this.teraType = Utils.randSeedItem(this.getTypes(false, false, true));
this.isTerastallized = false;
this.stellarTypesBoosted = [];
}
this.generateName();
@ -355,7 +367,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const getSprite = (hasShadow?: boolean) => {
const ret = globalScene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? "back__" : ""}sub`, undefined, true);
ret.setOrigin(0.5, 1);
ret.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: !!hasShadow, teraColor: getTypeRgb(this.getTeraType()) });
ret.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: !!hasShadow, teraColor: getTypeRgb(this.getTeraType()), isTerastallized: this.isTerastallized });
return ret;
};
@ -723,7 +735,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
updateSpritePipelineData(): void {
[ this.getSprite(), this.getTintSprite() ].filter(s => !!s).map(s => s.pipelineData["teraColor"] = getTypeRgb(this.getTeraType()));
[ this.getSprite(), this.getTintSprite() ].filter(s => !!s).map(s => {
s.pipelineData["teraColor"] = getTypeRgb(this.getTeraType());
s.pipelineData["isTerastallized"] = this.isTerastallized;
});
this.updateInfo(true);
}
@ -1208,6 +1223,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.fusionSpecies?.speciesId === species;
}
/**
* Checks if the {@linkcode Pokemon} has is the specified {@linkcode Species} or is fused with it.
* @param species the pokemon {@linkcode Species} to check
* @returns `true` if the pokemon is the species or is fused with it, `false` otherwise
*/
hasSpecies(species: Species): boolean {
return this.species.speciesId === species || this.fusionSpecies?.speciesId === species;
}
abstract isBoss(): boolean;
getMoveset(ignoreOverride?: boolean): (PokemonMove | null)[] {
@ -1284,9 +1308,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride: boolean = false): Type[] {
const types: Type[] = [];
if (includeTeraType) {
if (includeTeraType && this.isTerastallized) {
const teraType = this.getTeraType();
if (teraType !== Type.UNKNOWN) {
if (this.isTerastallized && !(forDefend && teraType === Type.STELLAR)) { // Stellar tera uses its original types defensively
types.push(teraType);
if (forDefend) {
return types;
@ -1590,23 +1614,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @returns the pokemon's current tera {@linkcode Type}, or `Type.UNKNOWN` if the pokemon is not terastallized
* @returns the pokemon's current tera {@linkcode Type}
*/
public getTeraType(): Type {
// I don't think this should be possible anymore, please report if you encounter this. --NightKev
if (globalScene === undefined) {
console.warn("Pokemon.getTeraType(): Global scene is not defined!");
return Type.UNKNOWN;
getTeraType(): Type {
if (this.hasSpecies(Species.TERAPAGOS)) {
return Type.STELLAR;
} else if (this.hasSpecies(Species.OGERPON)) {
const ogerponForm = this.species.speciesId === Species.OGERPON ? this.formIndex : this.fusionFormIndex;
switch (ogerponForm) {
case 0:
case 4:
return Type.GRASS;
case 1:
case 5:
return Type.WATER;
case 2:
case 6:
return Type.FIRE;
case 3:
case 7:
return Type.ROCK;
}
} else if (this.hasSpecies(Species.SHEDINJA)) {
return Type.BUG;
}
const teraModifier = globalScene.findModifier(m =>
m instanceof TerastallizeModifier
&& m.pokemonId === this.id
&& m.getBattlesLeft() > 0, this.isPlayer()) as TerastallizeModifier;
return teraModifier?.teraType ?? Type.UNKNOWN;
}
public isTerastallized(): boolean {
return this.getTeraType() !== Type.UNKNOWN;
return this.teraType;
}
public isGrounded(): boolean {
@ -1748,7 +1780,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier {
if (moveType === Type.STELLAR) {
return this.isTerastallized() ? 2 : 1;
return this.isTerastallized ? 2 : 1;
}
const types = this.getTypes(true, true);
const arena = globalScene.arena;
@ -2785,11 +2817,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const matchesSourceType = sourceTypes.includes(moveType);
/** A damage multiplier for when the attack is of the attacker's type and/or Tera type. */
const stabMultiplier = new Utils.NumberHolder(1);
if (matchesSourceType) {
stabMultiplier.value += 0.5;
}
applyMoveAttrs(CombinedPledgeStabBoostAttr, source, this, move, stabMultiplier);
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) {
if (matchesSourceType && moveType !== Type.STELLAR) {
stabMultiplier.value += 0.5;
}
@ -2797,6 +2825,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier);
}
applyMoveAttrs(CombinedPledgeStabBoostAttr, source, this, move, stabMultiplier);
if (source.isTerastallized && sourceTeraType === moveType && moveType !== Type.STELLAR) {
stabMultiplier.value += 0.5;
}
if (source.isTerastallized && source.teraType === Type.STELLAR && (!source.stellarTypesBoosted.includes(moveType) || source.hasSpecies(Species.TERAPAGOS))) {
if (matchesSourceType) {
stabMultiplier.value += 0.5;
} else {
stabMultiplier.value += 0.2;
}
}
stabMultiplier.value = Math.min(stabMultiplier.value, 2.25);
/** Halves damage if the attacker is using a physical attack while burned */
@ -3837,6 +3879,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
resetTera(): void {
const wasTerastallized = this.isTerastallized;
this.isTerastallized = false;
this.stellarTypesBoosted = [];
if (wasTerastallized) {
this.updateSpritePipelineData();
globalScene.triggerPokemonFormChange(this, SpeciesFormChangeLapseTeraTrigger);
}
}
resetTurnData(): void {
this.turnData = new PokemonTurnData();
}
@ -4600,6 +4652,7 @@ export class PlayerPokemon extends Pokemon {
newPokemon.fusionVariant = this.fusionVariant;
newPokemon.fusionGender = this.fusionGender;
newPokemon.fusionLuck = this.fusionLuck;
newPokemon.fusionTeraType = this.teraType;
newPokemon.usedTMs = this.usedTMs;
globalScene.getPlayerParty().push(newPokemon);
@ -4751,6 +4804,7 @@ export class EnemyPokemon extends Pokemon {
public aiType: AiType;
public bossSegments: number;
public bossSegmentIndex: number;
public initialTeamIndex: number;
/** To indicate if the instance was populated with a dataSource -> e.g. loaded & populated from session data */
public readonly isPopulatedFromDataSource: boolean;
@ -4760,6 +4814,7 @@ export class EnemyPokemon extends Pokemon {
undefined, dataSource ? dataSource.nature : undefined, dataSource);
this.trainerSlot = trainerSlot;
this.initialTeamIndex = globalScene.currentBattle?.enemyParty.length ?? 0;
this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource
if (boss) {
this.setBoss(boss, dataSource?.bossSegments);

View File

@ -11,7 +11,8 @@ import {
TrainerSlot,
trainerConfigs,
trainerPartyTemplates,
signatureSpecies
signatureSpecies,
TeraAIMode
} from "#app/data/trainer-config";
import type { EnemyPokemon } from "#app/field/pokemon";
import * as Utils from "#app/utils";
@ -36,6 +37,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
public partyTemplateIndex: number;
public name: string;
public partnerName: string;
public originalIndexes: { [key: number]: number } = {};
constructor(trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: number, name?: string, partnerName?: string, trainerConfigOverride?: TrainerConfig) {
super(globalScene, -72, 80);
@ -546,6 +548,13 @@ export default class Trainer extends Phaser.GameObjects.Container {
return [];
}
genAI(party: EnemyPokemon[]) {
if (this.config.genAIFuncs) {
this.config.genAIFuncs.forEach(f => f(party));
}
console.log("Generated AI funcs");
}
loadAssets(): Promise<void> {
return this.config.loadAssets(this.variant);
}
@ -667,4 +676,13 @@ export default class Trainer extends Phaser.GameObjects.Container {
}
});
}
shouldTera(pokemon: EnemyPokemon): boolean {
if (this.config.trainerAI.teraMode === TeraAIMode.INSTANT_TERA) {
if (!pokemon.isTerastallized && this.config.trainerAI.instantTeras.includes(pokemon.initialTeamIndex)) {
return true;
}
}
return false;
}
}

View File

@ -103,6 +103,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("icon_tera", "ui");
this.loadImage("type_tera", "ui");
this.loadAtlas("type_bgs", "ui");
this.loadAtlas("button_tera", "ui");
this.loadImage("mystery_egg", "ui");
this.loadImage("normal_memory", "ui");

View File

@ -11,7 +11,7 @@ import { Type } from "#enums/type";
import type { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier";
import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerrastalizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables";
@ -19,7 +19,7 @@ import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system
import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import PartyUiHandler from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text";
import { formatMoney, getEnumKeys, getEnumValues, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
import { formatMoney, getEnumKeys, getEnumValues, isNullOrUndefined, NumberHolder, padInt, randSeedInt } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
@ -275,6 +275,36 @@ export class PokemonHeldItemModifierType extends PokemonModifierType {
}
}
export class TerastallizeModifierType extends PokemonModifierType {
private teraType: Type;
constructor(teraType: Type) {
super("", `${Type[teraType].toLowerCase()}_tera_shard`, (type, args) => new TerrastalizeModifier(type as TerastallizeModifierType, (args[0] as Pokemon).id, teraType),
(pokemon: PlayerPokemon) => {
if ([ pokemon.species.speciesId, pokemon.fusionSpecies?.speciesId ].filter(s => s === Species.TERAPAGOS || s === Species.OGERPON || s === Species.SHEDINJA).length > 0) {
return PartyUiHandler.NoEffectMessage;
}
return null;
},
"tera_shard");
this.teraType = teraType;
}
get name(): string {
return i18next.t("modifierType:ModifierType.TerastallizeModifierType.name", { teraType: i18next.t(`pokemonInfo:Type.${Type[this.teraType]}`) });
}
getDescription(): string {
return i18next.t("modifierType:ModifierType.TerastallizeModifierType.description", { teraType: i18next.t(`pokemonInfo:Type.${Type[this.teraType]}`) });
}
getPregenArgs(): any[] {
return [ this.teraType ];
}
}
export class PokemonHpRestoreModifierType extends PokemonModifierType {
protected restorePoints: number;
protected restorePercent: number;
@ -1192,28 +1222,6 @@ class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
}
}
export class TerastallizeModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
private teraType: Type;
constructor(teraType: Type) {
super("", `${Type[teraType].toLowerCase()}_tera_shard`, (type, args) => new TerastallizeModifier(type as TerastallizeModifierType, (args[0] as Pokemon).id, teraType), "tera_shard");
this.teraType = teraType;
}
get name(): string {
return i18next.t("modifierType:ModifierType.TerastallizeModifierType.name", { teraType: i18next.t(`pokemonInfo:Type.${Type[this.teraType]}`) });
}
getDescription(): string {
return i18next.t("modifierType:ModifierType.TerastallizeModifierType.description", { teraType: i18next.t(`pokemonInfo:Type.${Type[this.teraType]}`) });
}
getPregenArgs(): any[] {
return [ this.teraType ];
}
}
export class ContactHeldItemTransferChanceModifierType extends PokemonHeldItemModifierType {
private chancePercent: number;
@ -1469,14 +1477,21 @@ export const modifierTypes = {
if (!globalScene.getModifiers(TerastallizeAccessModifier).length) {
return null;
}
let type: Type;
if (!randSeedInt(3)) {
const partyMemberTypes = party.map(p => p.getTypes(false, false, true)).flat();
type = randSeedItem(partyMemberTypes);
} else {
type = randSeedInt(64) ? randSeedInt(18) as Type : Type.STELLAR;
const teraTypes: Type[] = [];
party.forEach(p => {
if (!(p.hasSpecies(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.SHEDINJA))) {
teraTypes.push(p.teraType);
}
});
let excludedType = Type.UNKNOWN;
if (teraTypes.length > 0 && teraTypes.filter(t => t === teraTypes[0]).length === teraTypes.length) {
excludedType = teraTypes[0];
}
return new TerastallizeModifierType(type);
let shardType = randSeedInt(64) ? randSeedInt(18) as Type : Type.STELLAR;
while (shardType === excludedType) {
shardType = randSeedInt(64) ? randSeedInt(18) as Type : Type.STELLAR;
}
return new TerastallizeModifierType(shardType);
}),
BERRY: () => new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
@ -1719,7 +1734,7 @@ const modifierPool: ModifierPool = {
return Math.min(Math.ceil(highestPartyLevel / 20), 4);
}, 4),
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3),
new WeightedModifierType(modifierTypes.TERA_SHARD, 1),
new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => party.filter(p => !(p.hasSpecies(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.SHEDINJA))).length > 0 ? 1 : 0),
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => {
if (party.filter(p => !p.fusionSpecies).length > 1) {
if (globalScene.gameMode.isSplicedOnly) {

View File

@ -3,16 +3,16 @@ import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry";
import { getLevelTotalExp } from "#app/data/exp";
import { allMoves } from "#app/data/move";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectHealText } from "#app/data/status-effect";
import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
import type { PlayerPokemon } from "#app/field/pokemon";
import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { EvolutionPhase } from "#app/phases/evolution-phase";
import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase";
import { LevelUpPhase } from "#app/phases/level-up-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { achvs } from "#app/system/achv";
import type { VoucherType } from "#app/system/voucher";
import { Command } from "#app/ui/command-ui-handler";
import { addTextObject, TextStyle } from "#app/ui/text";
@ -25,7 +25,7 @@ import type { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species";
import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
import type { Type } from "#enums/type";
import i18next from "i18next";
import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type";
import { Color, ShadowColor } from "#enums/color";
@ -786,72 +786,6 @@ export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModi
}
}
export class TerastallizeModifier extends LapsingPokemonHeldItemModifier {
public override type: TerastallizeModifierType;
public teraType: Type;
public isTransferable: boolean = false;
constructor(type: TerastallizeModifierType, pokemonId: number, teraType: Type, battlesLeft?: number, stackCount?: number) {
super(type, pokemonId, battlesLeft || 10, stackCount);
this.teraType = teraType;
}
matchType(modifier: Modifier): boolean {
if (modifier instanceof TerastallizeModifier && modifier.teraType === this.teraType) {
return true;
}
return false;
}
clone(): TerastallizeModifier {
return new TerastallizeModifier(this.type, this.pokemonId, this.teraType, this.battlesLeft, this.stackCount);
}
getArgs(): any[] {
return [ this.pokemonId, this.teraType, this.battlesLeft ];
}
/**
* Applies the {@linkcode TerastallizeModifier} to the specified {@linkcode Pokemon}.
* @param pokemon the {@linkcode Pokemon} to be terastallized
* @returns always `true`
*/
override apply(pokemon: Pokemon): boolean {
if (pokemon.isPlayer()) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeTeraTrigger);
globalScene.validateAchv(achvs.TERASTALLIZE);
if (this.teraType === Type.STELLAR) {
globalScene.validateAchv(achvs.STELLAR_TERASTALLIZE);
}
}
pokemon.updateSpritePipelineData();
return true;
}
/**
* Triggers {@linkcode LapsingPokemonHeldItemModifier.lapse} and if it returns `0` a form change is triggered.
* @param pokemon THe {@linkcode Pokemon} to be terastallized
* @returns the result of {@linkcode LapsingPokemonHeldItemModifier.lapse}
*/
public override lapse(pokemon: Pokemon): boolean {
const ret = super.lapse(pokemon);
if (!ret) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeLapseTeraTrigger);
pokemon.updateSpritePipelineData();
}
return ret;
}
getScoreMultiplier(): number {
return 1.25;
}
getMaxHeldItemCount(pokemon: Pokemon): number {
return 1;
}
}
/**
* Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that
* increase the value of a given {@linkcode PermanentStat}.
@ -2022,6 +1956,36 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
}
}
export class TerrastalizeModifier extends ConsumablePokemonModifier {
public override type: TerastallizeModifierType;
public teraType: Type;
constructor(type: TerastallizeModifierType, pokemonId: number, teraType: Type) {
super(type, pokemonId);
this.teraType = teraType;
}
/**
* Checks if {@linkcode TerrastalizeModifier} should be applied
* @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item
* @returns `true` if the {@linkcode TerrastalizeModifier} should be applied
*/
override shouldApply(playerPokemon?: PlayerPokemon): boolean {
return super.shouldApply(playerPokemon) && [ playerPokemon?.species.speciesId, playerPokemon?.fusionSpecies?.speciesId ].filter(s => s === Species.TERAPAGOS || s === Species.OGERPON || s === Species.SHEDINJA).length === 0;
}
/**
* Applies {@linkcode TerrastalizeModifier}
* @param pokemon The {@linkcode PlayerPokemon} that consumes the item
* @returns `true` if hp was restored
*/
override apply(pokemon: Pokemon): boolean {
pokemon.teraType = this.teraType;
return true;
}
}
export class PokemonHpRestoreModifier extends ConsumablePokemonModifier {
private restorePoints: number;
private restorePercent: number;

View File

@ -118,6 +118,7 @@ export class CommandPhase extends FieldPhase {
let success: boolean = false;
switch (command) {
case Command.TERA:
case Command.FIGHT:
let useStruggle = false;
const turnMove: TurnMove | undefined = (args.length === 2 ? (args[1] as TurnMove) : undefined);
@ -137,6 +138,7 @@ export class CommandPhase extends FieldPhase {
}
const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args };
const preTurnCommand: TurnCommand = { command: command, targets: [ this.fieldIndex ], skip: command === Command.FIGHT };
const moveTargets: MoveTargetSet = turnMove === undefined ? getMoveTargets(playerPokemon, moveId) : { targets: turnMove.targets, multiple: turnMove.targets.length > 1 };
if (!moveId) {
turnCommand.targets = [ this.fieldIndex ];
@ -152,6 +154,7 @@ export class CommandPhase extends FieldPhase {
} else {
globalScene.unshiftPhase(new SelectTargetPhase(this.fieldIndex));
}
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
success = true;
} else if (cursor < playerPokemon.getMoveset().length) {

View File

@ -81,6 +81,10 @@ export class EnemyCommandPhase extends FieldPhase {
/** Select a move to use (and a target to use it against, if applicable) */
const nextMove = enemyPokemon.getNextMove();
if (trainer && trainer.shouldTera(enemyPokemon)) {
globalScene.currentBattle.preTurnCommands[this.fieldIndex + BattlerIndex.ENEMY] = { command: Command.TERA };
}
globalScene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] =
{ command: Command.FIGHT, move: nextMove, skip: this.skipTurn };

View File

@ -116,7 +116,7 @@ export class EvolutionPhase extends Phase {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) });
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()), isTerastallized: this.pokemon.isTerastallized });
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());
sprite.setPipelineData("shiny", this.pokemon.shiny);

View File

@ -108,6 +108,8 @@ export class FaintPhase extends PokemonPhase {
globalScene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true);
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
pokemon.resetTera();
if (pokemon.turnData?.attacksReceived?.length) {
const lastAttack = pokemon.turnData.attacksReceived[0];
applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, globalScene.getPokemonById(lastAttack.sourceId)!, new PokemonMove(lastAttack.move).getMove(), lastAttack.result); // TODO: is this bang correct?

View File

@ -450,6 +450,12 @@ export class MoveEffectPhase extends PokemonPhase {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
});
const moveType = user.getMoveType(move, true);
if (move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);
}
this.end();
});
});

View File

@ -36,5 +36,6 @@ export class PartyHealPhase extends BattlePhase {
globalScene.ui.fadeIn(500).then(() => this.end());
});
});
globalScene.arena.playerTerasUsed = 0;
}
}

View File

@ -1,7 +1,7 @@
import { globalScene } from "#app/global-scene";
import { SemiInvulnerableTag } from "#app/data/battler-tags";
import type { SpeciesFormChange } from "#app/data/pokemon-forms";
import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms";
import { getSpeciesFormChangeMessage, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { getTypeRgb } from "#app/data/type";
import { BattleSpec } from "#app/enums/battle-spec";
import { BattlerTagType } from "#app/enums/battler-tag-type";
@ -11,6 +11,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { BattlePhase } from "./battle-phase";
import { MovePhase } from "./move-phase";
import { PokemonHealPhase } from "./pokemon-heal-phase";
import { applyAbAttrs, PostTeraFormChangeStatChangeAbAttr } from "#app/data/ability";
export class QuietFormChangePhase extends BattlePhase {
protected pokemon: Pokemon;
@ -51,7 +52,7 @@ export class QuietFormChangePhase extends BattlePhase {
} catch (err: unknown) {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) });
sprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()), isTerastallized: this.pokemon.isTerastallized });
[ "spriteColors", "fusionSpriteColors" ].map(k => {
if (this.pokemon.summonData?.speciesForm) {
k += "Base";
@ -145,6 +146,9 @@ export class QuietFormChangePhase extends BattlePhase {
movePhase.cancel();
}
}
if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) {
applyAbAttrs(PostTeraFormChangeStatChangeAbAttr, this.pokemon, null);
}
super.end();
}

51
src/phases/tera-phase.ts Normal file
View File

@ -0,0 +1,51 @@
import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { BattlePhase } from "./battle-phase";
import i18next from "i18next";
import { globalScene } from "#app/global-scene";
import { Type } from "#app/enums/type";
import { achvs } from "#app/system/achv";
import { SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
export class TeraPhase extends BattlePhase {
public pokemon: Pokemon;
constructor(pokemon: Pokemon) {
super();
this.pokemon = pokemon;
}
start() {
super.start();
console.log(this.pokemon.name, "terastallized to", Type[this.pokemon.teraType].toString());
globalScene.queueMessage(i18next.t("battle:pokemonTerastallized", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), type: i18next.t(`pokemonInfo:Type.${Type[this.pokemon.teraType]}`) }));
new CommonBattleAnim(CommonAnim.TERASTALLIZE, this.pokemon).play(false, () => {
this.end();
});
}
end() {
this.pokemon.isTerastallized = true;
this.pokemon.updateSpritePipelineData();
if (this.pokemon.isPlayer()) {
globalScene.arena.playerTerasUsed += 1;
}
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangeTeraTrigger);
if (this.pokemon.isPlayer()) {
globalScene.validateAchv(achvs.TERASTALLIZE);
if (this.pokemon.teraType === Type.STELLAR) {
globalScene.validateAchv(achvs.STELLAR_TERASTALLIZE);
}
}
super.end();
}
}

View File

@ -21,6 +21,7 @@ import { BattlerIndex } from "#app/battle";
import { TrickRoomTag } from "#app/data/arena-tag";
import { SwitchType } from "#enums/switch-type";
import { globalScene } from "#app/global-scene";
import { TeraPhase } from "./tera-phase";
export class TurnStartPhase extends FieldPhase {
constructor() {
@ -139,6 +140,20 @@ export class TurnStartPhase extends FieldPhase {
let orderIndex = 0;
for (const o of this.getSpeedOrder()) {
const pokemon = field[o];
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
if (preTurnCommand?.skip) {
continue;
}
switch (preTurnCommand?.command) {
case Command.TERA:
globalScene.pushPhase(new TeraPhase(pokemon));
}
}
for (const o of moveOrder) {
const pokemon = field[o];

View File

@ -351,7 +351,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
const data = sprite.pipelineData;
const tone = data["tone"] as number[];
const teraColor = data["teraColor"] as number[] ?? [ 0, 0, 0 ];
const teraColor = (data["isTerastallized"] as boolean) ? (data["teraColor"] as number[] ?? [ 0, 0, 0 ]) : [ 0, 0, 0 ];
const hasShadow = data["hasShadow"] as boolean;
const yShadowOffset = data["yShadowOffset"] as number;
const ignoreFieldPos = data["ignoreFieldPos"] as boolean;

View File

@ -10,12 +10,14 @@ export default class ArenaData {
public weather: Weather | null;
public terrain: Terrain | null;
public tags: ArenaTag[];
public playerTerasUsed: number;
constructor(source: Arena | any) {
const sourceArena = source instanceof Arena ? source as Arena : null;
this.biome = sourceArena ? sourceArena.biomeType : source.biome;
this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
this.playerTerasUsed = (sourceArena ? sourceArena.playerTerasUsed : source.playerTerasUsed) ?? 0;
this.tags = [];
if (source.tags) {

View File

@ -1087,6 +1087,8 @@ export class GameData {
globalScene.arena.terrain = sessionData.arena.terrain;
globalScene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(TerrainType.NONE, globalScene.arena.terrain?.terrainType!, globalScene.arena.terrain?.turnsLeft!)); // TODO: is this bang correct?
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed;
globalScene.arena.tags = sessionData.arena.tags;
if (globalScene.arena.tags) {
for (const tag of globalScene.arena.tags) {

View File

@ -13,6 +13,7 @@ import type { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import type { Species } from "#enums/species";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import type { Type } from "#app/enums/type";
export default class PokemonData {
public id: number;
@ -45,6 +46,9 @@ export default class PokemonData {
public pokerus: boolean;
public usedTMs: Moves[];
public evoCounter: number;
public teraType: Type;
public isTerastallized: boolean;
public stellarTypesBoosted: Type[];
public fusionSpecies: Species;
public fusionFormIndex: number;
@ -53,6 +57,7 @@ export default class PokemonData {
public fusionVariant: Variant;
public fusionGender: Gender;
public fusionLuck: number;
public fusionTeraType: Type;
public boss: boolean;
public bossSegments?: number;
@ -103,6 +108,9 @@ export default class PokemonData {
this.evoCounter = source.evoCounter ?? 0;
}
this.pokerus = !!source.pokerus;
this.teraType = source.teraType as Type;
this.isTerastallized = source.isTerastallized || false;
this.stellarTypesBoosted = source.stellarTypesBoosted || [];
this.fusionSpecies = sourcePokemon ? sourcePokemon.fusionSpecies?.speciesId : source.fusionSpecies;
this.fusionFormIndex = source.fusionFormIndex;
@ -112,6 +120,7 @@ export default class PokemonData {
this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
this.fusionTeraType = (source.fusionTeraType ?? 0) as Type;
this.usedTMs = source.usedTMs ?? [];
this.customPokemonData = new CustomPokemonData(source.customPokemonData);

View File

@ -7,6 +7,9 @@ import * as v1_0_4 from "./versions/v1_0_4";
// --- v1.1.0 PATCHES --- //
import * as v1_1_0 from "./versions/v1_1_0";
// --- v1.7.0 PATCHES --- //
import * as v1_7_0 from "./versions/v1_7_0";
const LATEST_VERSION = version.split(".").map(value => parseInt(value));
/**
@ -138,6 +141,10 @@ class SessionVersionConverter extends VersionConverter {
console.log("Applying v1.1.0 session data migration!");
this.callMigrators(data, v1_1_0.sessionMigrators);
}
if (curMinor < 7) {
console.log("Applying v1.7.0 session data migration!");
this.callMigrators(data, v1_7_0.sessionMigrators);
}
}
console.log(`Session data successfully migrated to v${version}!`);
@ -164,6 +171,10 @@ class SystemVersionConverter extends VersionConverter {
console.log("Applying v1.1.0 system data migraton!");
this.callMigrators(data, v1_1_0.systemMigrators);
}
if (curMinor < 7) {
console.log("Applying v1.7.0 session data migration!");
this.callMigrators(data, v1_7_0.systemMigrators);
}
}
console.log(`System data successfully migrated to v${version}!`);
@ -190,6 +201,10 @@ class SettingsVersionConverter extends VersionConverter {
console.log("Applying v1.1.0 settings data migraton!");
this.callMigrators(data, v1_1_0.settingsMigrators);
}
if (curMinor < 7) {
console.log("Applying v1.7.0 session data migration!");
this.callMigrators(data, v1_7_0.settingsMigrators);
}
}
console.log(`System data successfully migrated to v${version}!`);

View File

@ -0,0 +1,49 @@
import { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import type { SessionSaveData } from "#app/system/game-data";
import * as Utils from "#app/utils";
export const systemMigrators = [] as const;
export const settingsMigrators = [] as const;
export const sessionMigrators = [
function migrateTera(data: SessionSaveData) {
for (let i = 0; i < data.modifiers.length;) {
if (data.modifiers[i].className === "TerastallizeModifier") {
data.party.forEach((p) => {
if (p.id === data.modifiers[i].args[0]) {
p.teraType = data.modifiers[i].args[1];
}
});
data.modifiers.splice(i, 1);
} else {
i++;
}
}
for (let i = 0; i < data.enemyModifiers.length;) {
if (data.enemyModifiers[i].className === "TerastallizeModifier") {
data.enemyParty.forEach((p) => {
if (p.id === data.enemyModifiers[i].args[0]) {
p.teraType = data.enemyModifiers[i].args[1];
}
});
data.enemyModifiers.splice(i, 1);
} else {
i++;
}
}
data.party.forEach(p => {
if (Utils.isNullOrUndefined(p.teraType)) {
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
}
});
data.enemyParty.forEach(p => {
if (Utils.isNullOrUndefined(p.teraType)) {
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
}
});
}
] as const;

View File

@ -11,7 +11,7 @@ import { Species } from "#enums/species";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
describe("Abilities - Libero", () => {
@ -258,7 +258,7 @@ describe("Abilities - Libero", () => {
const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
vi.spyOn(leadPokemon, "isTerastallized").mockReturnValue(true);
leadPokemon.isTerastallized = true;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);

View File

@ -11,7 +11,7 @@ import { Species } from "#enums/species";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
describe("Abilities - Protean", () => {
@ -258,7 +258,7 @@ describe("Abilities - Protean", () => {
const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
vi.spyOn(leadPokemon, "isTerastallized").mockReturnValue(true);
leadPokemon.isTerastallized = true;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);

View File

@ -6,7 +6,6 @@ import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import * as Messages from "#app/messages";
import { TerastallizeModifier, overrideHeldItems } from "#app/modifier/modifier";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@ -15,15 +14,14 @@ function testMoveEffectiveness(game: GameManager, move: Moves, targetSpecies: Sp
expected: number, targetAbility: Abilities = Abilities.BALL_FETCH, teraType?: Type): void {
// Suppress getPokemonNameWithAffix because it calls on a null battle spec
vi.spyOn(Messages, "getPokemonNameWithAffix").mockReturnValue("");
game.override
.enemyAbility(targetAbility)
.enemyHeldItems([{ name:"TERA_SHARD", type: teraType }]);
game.override.enemyAbility(targetAbility);
const user = game.scene.addPlayerPokemon(getPokemonSpecies(Species.SNORLAX), 5);
const target = game.scene.addEnemyPokemon(getPokemonSpecies(targetSpecies), 5, TrainerSlot.NONE);
if (teraType !== undefined) {
overrideHeldItems(target, false);
target.teraType = teraType;
target.isTerastallized = true;
}
expect(target.getMoveEffectiveness(user, allMoves[move])).toBe(expected);
@ -40,7 +38,6 @@ describe("Moves - Type Effectiveness", () => {
type: Phaser.HEADLESS,
});
game = new GameManager(phaserGame);
TerastallizeModifier.prototype.apply = (args) => true;
game.override.ability(Abilities.BALL_FETCH);
});

View File

@ -117,11 +117,12 @@ describe("Moves - Freeze-Dry", () => {
});
it("should deal 2x damage to steel type terastallized into water", async () => {
game.override.enemySpecies(Species.SKARMORY)
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]);
game.override.enemySpecies(Species.SKARMORY);
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
enemy.teraType = Type.WATER;
enemy.isTerastallized = true;
vi.spyOn(enemy, "getMoveEffectiveness");
game.move.select(Moves.FREEZE_DRY);
@ -132,11 +133,12 @@ describe("Moves - Freeze-Dry", () => {
});
it("should deal 0.5x damage to water type terastallized into fire", async () => {
game.override.enemySpecies(Species.PELIPPER)
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]);
game.override.enemySpecies(Species.PELIPPER);
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
enemy.teraType = Type.FIRE;
enemy.isTerastallized = true;
vi.spyOn(enemy, "getMoveEffectiveness");
game.move.select(Moves.FREEZE_DRY);

View File

@ -83,10 +83,12 @@ describe("Moves - Tar Shot", () => {
});
it("does not double the effectiveness of Fire-type moves against a Pokémon that is Terastallized", async () => {
game.override.enemyHeldItems([{ name: "TERA_SHARD", type: Type.GRASS }]).enemySpecies(Species.SPRIGATITO);
game.override.enemySpecies(Species.SPRIGATITO);
await game.classicMode.startBattle([ Species.PIKACHU ]);
const enemy = game.scene.getEnemyPokemon()!;
enemy.teraType = Type.GRASS;
enemy.isTerastallized = true;
vi.spyOn(enemy, "getMoveEffectiveness");
@ -119,7 +121,8 @@ describe("Moves - Tar Shot", () => {
await game.toNextTurn();
game.override.enemyHeldItems([{ name: "TERA_SHARD", type: Type.GRASS }]);
enemy.teraType = Type.GRASS;
enemy.isTerastallized = true;
game.move.select(Moves.FIRE_PUNCH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);

View File

@ -35,7 +35,6 @@ describe("Moves - Tera Blast", () => {
.starterSpecies(Species.FEEBAS)
.moveset([ Moves.TERA_BLAST ])
.ability(Abilities.BALL_FETCH)
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }])
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.STURDY)
@ -45,13 +44,15 @@ describe("Moves - Tera Blast", () => {
});
it("changes type to match user's tera type", async () => {
game.override
.enemySpecies(Species.FURRET)
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIGHTING }]);
game.override.enemySpecies(Species.FURRET);
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = Type.FIGHTING;
playerPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
@ -60,10 +61,12 @@ describe("Moves - Tera Blast", () => {
}, 20000);
it("increases power if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = Type.STELLAR;
playerPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
@ -72,13 +75,15 @@ describe("Moves - Tera Blast", () => {
}, 20000);
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = Type.STELLAR;
playerPokemon.isTerastallized = true;
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true);
enemyPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
@ -93,6 +98,7 @@ describe("Moves - Tera Blast", () => {
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 100;
playerPokemon.stats[Stat.SPATK] = 1;
playerPokemon.isTerastallized = true;
vi.spyOn(teraBlastAttr, "apply");
@ -169,10 +175,11 @@ describe("Moves - Tera Blast", () => {
it("causes stat drops if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = Type.STELLAR;
playerPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);

View File

@ -29,8 +29,7 @@ describe("Moves - Tera Starstorm", () => {
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(30)
.enemySpecies(Species.MAGIKARP)
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]);
.enemySpecies(Species.MAGIKARP);
});
it("changes type to Stellar when used by Terapagos in its Stellar Form", async () => {
@ -38,19 +37,22 @@ describe("Moves - Tera Starstorm", () => {
await game.classicMode.startBattle([ Species.TERAPAGOS ]);
const terapagos = game.scene.getPlayerPokemon()!;
terapagos.isTerastallized = true;
vi.spyOn(terapagos, "getMoveType");
game.move.select(Moves.TERA_STARSTORM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(terapagos.isTerastallized()).toBe(true);
expect(terapagos.getMoveType).toHaveReturnedWith(Type.STELLAR);
});
it("targets both opponents in a double battle when used by Terapagos in its Stellar Form", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP, Species.TERAPAGOS ]);
const terapagos = game.scene.getPlayerParty()[1];
terapagos.isTerastallized = true;
game.move.select(Moves.TERA_STARSTORM, 0, BattlerIndex.ENEMY);
game.move.select(Moves.TERA_STARSTORM, 1);
@ -82,6 +84,8 @@ describe("Moves - Tera Starstorm", () => {
fusionedMon.fusionGender = magikarp.gender;
fusionedMon.fusionLuck = magikarp.luck;
fusionedMon.isTerastallized = true;
vi.spyOn(fusionedMon, "getMoveType");
game.move.select(Moves.TERA_STARSTORM, 0);
@ -90,7 +94,6 @@ describe("Moves - Tera Starstorm", () => {
// Fusion and terastallized
expect(fusionedMon.isFusion()).toBe(true);
expect(fusionedMon.isTerastallized()).toBe(true);
// Move effects should be applied
expect(fusionedMon.getMoveType).toHaveReturnedWith(Type.STELLAR);
expect(game.scene.getEnemyField().every(pokemon => pokemon.isFullHp())).toBe(false);

View File

@ -324,9 +324,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.lastTeraType = pokemon.getTeraType();
this.teraIcon.setPositionRelative(this.nameText, nameTextWidth + this.genderText.displayWidth + 1, 2);
this.teraIcon.setVisible(this.lastTeraType !== Type.UNKNOWN);
this.teraIcon.setVisible(pokemon.isTerastallized);
this.teraIcon.on("pointerover", () => {
if (this.lastTeraType !== Type.UNKNOWN) {
if (pokemon.isTerastallized) {
globalScene.ui.showTooltip("", i18next.t("fightUiHandler:teraHover", { type: i18next.t(`pokemonInfo:Type.${Type[this.lastTeraType]}`) }));
}
});
@ -542,7 +542,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.genderText.setPositionRelative(this.nameText, this.nameText.displayWidth, 0);
}
const teraType = pokemon.getTeraType();
const teraType = pokemon.isTerastallized ? pokemon.getTeraType() : Type.UNKNOWN;
const teraTypeUpdated = this.lastTeraType !== teraType;
if (teraTypeUpdated) {

View File

@ -7,18 +7,24 @@ import { Button } from "#enums/buttons";
import { getPokemonNameWithAffix } from "#app/messages";
import { CommandPhase } from "#app/phases/command-phase";
import { globalScene } from "#app/global-scene";
import { TerastallizeAccessModifier } from "#app/modifier/modifier";
import { Type } from "#app/enums/type";
import { getTypeRgb } from "#app/data/type";
export enum Command {
FIGHT = 0,
BALL,
POKEMON,
RUN
RUN,
TERA
}
export default class CommandUiHandler extends UiHandler {
private commandsContainer: Phaser.GameObjects.Container;
private cursorObj: Phaser.GameObjects.Image | null;
private teraButton: Phaser.GameObjects.Sprite;
protected fieldIndex: number = 0;
protected cursor2: number = 0;
@ -40,6 +46,13 @@ export default class CommandUiHandler extends UiHandler {
this.commandsContainer.setVisible(false);
ui.add(this.commandsContainer);
this.teraButton = globalScene.add.sprite(-32, 15, "button_tera");
this.teraButton.setName("terrastallize-button");
this.teraButton.setScale(1.3);
this.teraButton.setFrame("fire");
this.teraButton.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true, teraColor: getTypeRgb(Type.FIRE), isTerastallized: false });
this.commandsContainer.add(this.teraButton);
for (let c = 0; c < commands.length; c++) {
const commandText = addTextObject(c % 2 === 0 ? 0 : 55.8, c < 2 ? 0 : 16, commands[c], TextStyle.WINDOW);
commandText.setName(commands[c]);
@ -62,11 +75,22 @@ export default class CommandUiHandler extends UiHandler {
commandPhase = globalScene.getStandbyPhase() as CommandPhase;
}
if (this.canTera()) {
this.teraButton.setVisible(true);
this.teraButton.setFrame(Type[globalScene.getField()[this.fieldIndex].getTeraType()].toLowerCase());
} else {
this.teraButton.setVisible(false);
if (this.cursor === Command.TERA) {
this.setCursor(Command.FIGHT);
}
}
this.toggleTeraButton();
const messageHandler = this.getUi().getMessageHandler();
messageHandler.bg.setVisible(true);
messageHandler.commandWindow.setVisible(true);
messageHandler.movesWindowContainer.setVisible(false);
messageHandler.message.setWordWrapWidth(1110);
messageHandler.message.setWordWrapWidth(this.canTera() ? 910 : 1110);
messageHandler.showText(i18next.t("commandUiHandler:actionMessage", { pokemonName: getPokemonNameWithAffix(commandPhase.getPokemon()) }), 0);
if (this.getCursor() === Command.POKEMON) {
this.setCursor(Command.FIGHT);
@ -108,6 +132,10 @@ export default class CommandUiHandler extends UiHandler {
(globalScene.getCurrentPhase() as CommandPhase).handleCommand(Command.RUN, 0);
success = true;
break;
case Command.TERA:
ui.setMode(Mode.FIGHT, (globalScene.getCurrentPhase() as CommandPhase).getFieldIndex(), Command.TERA);
success = true;
break;
}
} else {
(globalScene.getCurrentPhase() as CommandPhase).cancel();
@ -115,23 +143,29 @@ export default class CommandUiHandler extends UiHandler {
} else {
switch (button) {
case Button.UP:
if (cursor >= 2) {
if (cursor === Command.POKEMON || cursor === Command.RUN) {
success = this.setCursor(cursor - 2);
}
break;
case Button.DOWN:
if (cursor < 2) {
if (cursor === Command.FIGHT || cursor === Command.BALL) {
success = this.setCursor(cursor + 2);
}
break;
case Button.LEFT:
if (cursor % 2 === 1) {
if (cursor === Command.BALL || cursor === Command.RUN) {
success = this.setCursor(cursor - 1);
} else if ((cursor === Command.FIGHT || cursor === Command.POKEMON) && this.canTera()) {
success = this.setCursor(Command.TERA);
this.toggleTeraButton();
}
break;
case Button.RIGHT:
if (cursor % 2 === 0) {
if (cursor === Command.FIGHT || cursor === Command.POKEMON) {
success = this.setCursor(cursor + 1);
} else if (cursor === Command.TERA) {
success = this.setCursor(Command.FIGHT);
this.toggleTeraButton();
}
break;
}
@ -144,6 +178,17 @@ export default class CommandUiHandler extends UiHandler {
return success;
}
canTera(): boolean {
const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length;
const currentTeras = globalScene.arena.playerTerasUsed;
const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 ? 1 : 0;
return hasTeraMod && (currentTeras + plannedTera) < 1;
}
toggleTeraButton() {
this.teraButton.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true, teraColor: getTypeRgb(globalScene.getField()[this.fieldIndex].getTeraType()), isTerastallized: this.getCursor() === Command.TERA });
}
getCursor(): number {
return !this.fieldIndex ? this.cursor : this.cursor2;
}
@ -163,7 +208,12 @@ export default class CommandUiHandler extends UiHandler {
this.commandsContainer.add(this.cursorObj);
}
this.cursorObj.setPosition(-5 + (cursor % 2 === 1 ? 56 : 0), 8 + (cursor >= 2 ? 16 : 0));
if (cursor === Command.TERA) {
this.cursorObj.setVisible(false);
} else {
this.cursorObj.setPosition(-5 + (cursor % 2 === 1 ? 56 : 0), 8 + (cursor >= 2 ? 16 : 0));
this.cursorObj.setVisible(true);
}
return changed;
}

View File

@ -33,6 +33,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
private moveInfoOverlay : MoveInfoOverlay;
protected fieldIndex: number = 0;
protected fromCommand: Command = Command.FIGHT;
protected cursor2: number = 0;
constructor() {
@ -114,6 +115,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
super.show(args);
this.fieldIndex = args.length ? args[0] as number : 0;
this.fromCommand = args.length > 1 ? args[1] as Command : Command.FIGHT;
const messageHandler = this.getUi().getMessageHandler();
messageHandler.bg.setVisible(false);
@ -140,7 +142,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
if (button === Button.CANCEL || button === Button.ACTION) {
if (button === Button.ACTION) {
if ((globalScene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, cursor, false)) {
if ((globalScene.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false)) {
success = true;
} else {
ui.playError();

View File

@ -420,17 +420,6 @@ export default class RunInfoUiHandler extends UiHandler {
private parseTrainerDefeat(enemyContainer: Phaser.GameObjects.Container) {
// Loads and adds trainer sprites to the UI
this.showTrainerSprites(enemyContainer);
// Determining which Terastallize Modifier belongs to which Pokemon
// Creates a dictionary {PokemonId: TeraShardType}
const teraPokemon = {};
this.runInfo.enemyModifiers.forEach((m) => {
const modifier = m.toModifier(this.modifiersModule[m.className]);
if (modifier instanceof Modifier.TerastallizeModifier) {
const teraDetails = modifier?.getArgs();
const pkmnId = teraDetails[0];
teraPokemon[pkmnId] = teraDetails[1];
}
});
// Creates the Pokemon icons + level information and adds it to enemyContainer
// 2 Rows x 3 Columns
@ -444,18 +433,6 @@ export default class RunInfoUiHandler extends UiHandler {
enemyData["player"] = true;
const enemy = enemyData.toPokemon();
const enemyIcon = globalScene.addPokemonIcon(enemy, 0, 0, 0, 0);
// Applying Terastallizing Type tint to Pokemon icon
// If the Pokemon is a fusion, it has two sprites and so, the tint has to be applied to each icon separately
const enemySprite1 = enemyIcon.list[0] as Phaser.GameObjects.Sprite;
const enemySprite2 = (enemyIcon.list.length > 1) ? enemyIcon.list[1] as Phaser.GameObjects.Sprite : undefined;
if (teraPokemon[enemyData.id]) {
const teraTint = getTypeRgb(teraPokemon[enemyData.id]);
const teraColor = new Phaser.Display.Color(teraTint[0], teraTint[1], teraTint[2]);
enemySprite1.setTint(teraColor.color);
if (enemySprite2) {
enemySprite2.setTint(teraColor.color);
}
}
enemyIcon.setPosition(39 * (e % 3) + 5, (35 * pokemonRowHeight));
const enemyLevel = addTextObject(43 * (e % 3), (27 * (pokemonRowHeight + 1)), `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, isBoss ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "54px" });
enemyLevel.setShadow(0, 0, undefined);

View File

@ -334,6 +334,7 @@ export default class SummaryUiHandler extends UiHandler {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
this.pokemonSprite.setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType()));
this.pokemonSprite.setPipelineData("isTerastallized", this.pokemon.isTerastallized);
this.pokemonSprite.setPipelineData("ignoreTimeTint", true);
this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());
this.pokemonSprite.setPipelineData("shiny", this.pokemon.shiny);
@ -782,7 +783,7 @@ export default class SummaryUiHandler extends UiHandler {
if (types.length > 1) {
profileContainer.add(getTypeIcon(1, types[1]));
}
if (this.pokemon?.isTerastallized()) {
if (this.pokemon?.isTerastallized) {
profileContainer.add(getTypeIcon(types.length, this.pokemon.getTeraType(), true));
}