adds 2 new encounters and colorable text options

This commit is contained in:
ImperialSympathizer 2024-07-08 17:30:24 -04:00
parent 872242c44f
commit 7b35efe95e
24 changed files with 2235 additions and 121 deletions

View File

@ -0,0 +1,734 @@
{
"textures": [
{
"image": "b2w2_lady.png",
"format": "RGBA8888",
"size": {
"w": 399,
"h": 360
},
"scale": 1,
"frames": [
{
"filename": "0000.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 0,
"y": 0,
"w": 56,
"h": 72
}
},
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 57,
"y": 0,
"w": 56,
"h": 72
}
},
{
"filename": "0002.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 114,
"y": 0,
"w": 56,
"h": 72
}
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 9,
"y": 8,
"w": 55,
"h": 72
},
"frame": {
"x": 171,
"y": 0,
"w": 55,
"h": 72
}
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 11,
"y": 8,
"w": 54,
"h": 72
},
"frame": {
"x": 228,
"y": 0,
"w": 54,
"h": 72
}
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 11,
"y": 8,
"w": 54,
"h": 72
},
"frame": {
"x": 285,
"y": 0,
"w": 54,
"h": 72
}
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 14,
"y": 8,
"w": 52,
"h": 72
},
"frame": {
"x": 342,
"y": 0,
"w": 52,
"h": 72
}
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 20,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 0,
"y": 72,
"w": 48,
"h": 72
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 47,
"h": 72
},
"frame": {
"x": 57,
"y": 72,
"w": 47,
"h": 72
}
},
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 47,
"h": 72
},
"frame": {
"x": 114,
"y": 72,
"w": 47,
"h": 72
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 171,
"y": 72,
"w": 48,
"h": 72
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 228,
"y": 72,
"w": 48,
"h": 72
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 285,
"y": 72,
"w": 48,
"h": 72
}
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 342,
"y": 72,
"w": 48,
"h": 72
}
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 49,
"h": 72
},
"frame": {
"x": 0,
"y": 144,
"w": 49,
"h": 72
}
},
{
"filename": "0015.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 49,
"h": 72
},
"frame": {
"x": 57,
"y": 144,
"w": 49,
"h": 72
}
},
{
"filename": "0016.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 49,
"h": 72
},
"frame": {
"x": 114,
"y": 144,
"w": 49,
"h": 72
}
},
{
"filename": "0017.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 49,
"h": 72
},
"frame": {
"x": 171,
"y": 144,
"w": 49,
"h": 72
}
},
{
"filename": "0018.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 228,
"y": 144,
"w": 48,
"h": 72
}
},
{
"filename": "0019.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 285,
"y": 144,
"w": 48,
"h": 72
}
},
{
"filename": "0020.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 342,
"y": 144,
"w": 48,
"h": 72
}
},
{
"filename": "0021.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 22,
"y": 8,
"w": 48,
"h": 72
},
"frame": {
"x": 0,
"y": 216,
"w": 48,
"h": 72
}
},
{
"filename": "0022.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 20,
"y": 8,
"w": 50,
"h": 72
},
"frame": {
"x": 57,
"y": 216,
"w": 50,
"h": 72
}
},
{
"filename": "0023.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 18,
"y": 8,
"w": 51,
"h": 72
},
"frame": {
"x": 114,
"y": 216,
"w": 51,
"h": 72
}
},
{
"filename": "0024.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 18,
"y": 8,
"w": 51,
"h": 72
},
"frame": {
"x": 171,
"y": 216,
"w": 51,
"h": 72
}
},
{
"filename": "0025.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 15,
"y": 8,
"w": 53,
"h": 72
},
"frame": {
"x": 228,
"y": 216,
"w": 53,
"h": 72
}
},
{
"filename": "0026.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 10,
"y": 8,
"w": 57,
"h": 72
},
"frame": {
"x": 285,
"y": 216,
"w": 57,
"h": 72
}
},
{
"filename": "0027.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 10,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 342,
"y": 216,
"w": 56,
"h": 72
}
},
{
"filename": "0028.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 10,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 0,
"y": 288,
"w": 56,
"h": 72
}
},
{
"filename": "0029.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 9,
"y": 8,
"w": 55,
"h": 72
},
"frame": {
"x": 57,
"y": 288,
"w": 55,
"h": 72
}
},
{
"filename": "0030.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 114,
"y": 288,
"w": 56,
"h": 72
}
},
{
"filename": "0031.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 171,
"y": 288,
"w": 56,
"h": 72
}
},
{
"filename": "0032.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 228,
"y": 288,
"w": 56,
"h": 72
}
},
{
"filename": "0033.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 8,
"y": 8,
"w": 56,
"h": 72
},
"frame": {
"x": 285,
"y": 288,
"w": 56,
"h": 72
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:e7f062304401dbd7b3ec79512f0ff4cb:0136dac01331f88892a3df26aeab78f5:1ed1e22abb9b55d76337a5a599835c06$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,797 @@
{
"textures": [
{
"image": "b2w2_veteran_m.png",
"format": "RGBA8888",
"size": {
"w": 424,
"h": 390
},
"scale": 1,
"frames": [
{
"filename": "0000.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 0,
"y": 0,
"w": 43,
"h": 78
}
},
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 53,
"y": 0,
"w": 43,
"h": 78
}
},
{
"filename": "0002.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 106,
"y": 0,
"w": 43,
"h": 78
}
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 159,
"y": 0,
"w": 43,
"h": 78
}
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 212,
"y": 0,
"w": 44,
"h": 78
}
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 265,
"y": 0,
"w": 44,
"h": 78
}
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 318,
"y": 0,
"w": 44,
"h": 78
}
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 371,
"y": 0,
"w": 44,
"h": 78
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 0,
"y": 78,
"w": 44,
"h": 78
}
},
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 53,
"y": 78,
"w": 44,
"h": 78
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 48,
"h": 78
},
"frame": {
"x": 106,
"y": 78,
"w": 48,
"h": 78
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 50,
"h": 78
},
"frame": {
"x": 159,
"y": 78,
"w": 50,
"h": 78
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 53,
"h": 78
},
"frame": {
"x": 212,
"y": 78,
"w": 53,
"h": 78
}
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 53,
"h": 78
},
"frame": {
"x": 265,
"y": 78,
"w": 53,
"h": 78
}
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 52,
"h": 78
},
"frame": {
"x": 318,
"y": 78,
"w": 52,
"h": 78
}
},
{
"filename": "0015.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 51,
"h": 78
},
"frame": {
"x": 371,
"y": 78,
"w": 51,
"h": 78
}
},
{
"filename": "0016.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 52,
"h": 78
},
"frame": {
"x": 0,
"y": 156,
"w": 52,
"h": 78
}
},
{
"filename": "0017.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 52,
"h": 78
},
"frame": {
"x": 53,
"y": 156,
"w": 52,
"h": 78
}
},
{
"filename": "0018.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 53,
"h": 78
},
"frame": {
"x": 106,
"y": 156,
"w": 53,
"h": 78
}
},
{
"filename": "0019.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 53,
"h": 78
},
"frame": {
"x": 159,
"y": 156,
"w": 53,
"h": 78
}
},
{
"filename": "0020.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 53,
"h": 78
},
"frame": {
"x": 212,
"y": 156,
"w": 53,
"h": 78
}
},
{
"filename": "0021.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 52,
"h": 78
},
"frame": {
"x": 265,
"y": 156,
"w": 52,
"h": 78
}
},
{
"filename": "0022.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 51,
"h": 78
},
"frame": {
"x": 318,
"y": 156,
"w": 51,
"h": 78
}
},
{
"filename": "0023.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 51,
"h": 78
},
"frame": {
"x": 371,
"y": 156,
"w": 51,
"h": 78
}
},
{
"filename": "0024.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 51,
"h": 78
},
"frame": {
"x": 0,
"y": 234,
"w": 51,
"h": 78
}
},
{
"filename": "0025.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 50,
"h": 78
},
"frame": {
"x": 53,
"y": 234,
"w": 50,
"h": 78
}
},
{
"filename": "0026.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 48,
"h": 78
},
"frame": {
"x": 106,
"y": 234,
"w": 48,
"h": 78
}
},
{
"filename": "0027.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 46,
"h": 78
},
"frame": {
"x": 159,
"y": 234,
"w": 46,
"h": 78
}
},
{
"filename": "0028.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 46,
"h": 78
},
"frame": {
"x": 212,
"y": 234,
"w": 46,
"h": 78
}
},
{
"filename": "0029.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 265,
"y": 234,
"w": 44,
"h": 78
}
},
{
"filename": "0030.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 318,
"y": 234,
"w": 44,
"h": 78
}
},
{
"filename": "0031.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 371,
"y": 234,
"w": 44,
"h": 78
}
},
{
"filename": "0032.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 44,
"h": 78
},
"frame": {
"x": 0,
"y": 312,
"w": 44,
"h": 78
}
},
{
"filename": "0033.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 53,
"y": 312,
"w": 43,
"h": 78
}
},
{
"filename": "0034.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 106,
"y": 312,
"w": 43,
"h": 78
}
},
{
"filename": "0035.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 159,
"y": 312,
"w": 43,
"h": 78
}
},
{
"filename": "0036.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 13,
"y": 2,
"w": 43,
"h": 78
},
"frame": {
"x": 212,
"y": 312,
"w": 43,
"h": 78
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:4deb068879a8ac195cb4f00c8b17b7f5:b32f0f90436649264b6f3c49b09ac06a:05e903aa75b8e50c28334d9b5e14c85a$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,18 +1,18 @@
import { PlayerPokemon } from "#app/field/pokemon";
import { ModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import {PlayerPokemon} from "#app/field/pokemon";
import {ModifierType, PokemonHeldItemModifierType} from "#app/modifier/modifier-type";
import BattleScene from "../battle-scene";
import { isNullOrUndefined } from "../utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day";
import { Nature } from "./nature";
import { EvolutionItem, pokemonEvolutions } from "./pokemon-evolutions";
import { FormChangeItem, SpeciesFormChangeItemTrigger, pokemonFormChanges } from "./pokemon-forms";
import { SpeciesFormKey } from "./pokemon-species";
import { StatusEffect } from "./status-effect";
import { Type } from "./type";
import { WeatherType } from "./weather";
import {isNullOrUndefined} from "../utils";
import {Abilities} from "#enums/abilities";
import {Moves} from "#enums/moves";
import {Species} from "#enums/species";
import {TimeOfDay} from "#enums/time-of-day";
import {Nature} from "./nature";
import {EvolutionItem, pokemonEvolutions} from "./pokemon-evolutions";
import {FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger} from "./pokemon-forms";
import {SpeciesFormKey} from "./pokemon-species";
import {StatusEffect} from "./status-effect";
import {Type} from "./type";
import {WeatherType} from "./weather";
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
export interface EncounterRequirement {
@ -39,7 +39,7 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen
}
// Returns all party members that are compatible with this requirement. For non pokemon related requirements, the entire party is returned..
queryParty(partyPokemon: PlayerPokemon[]) {
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
return [];
}
@ -215,23 +215,31 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement {
}
export class MoneyRequirement extends EncounterSceneRequirement {
requiredMoney: number;
requiredMoney: number; // Static value
scalingMultiplier: number; // Calculates required money based off wave index
constructor(requiredMoney: number) {
constructor(requiredMoney: number, scalingMultiplier?: number) {
super();
this.requiredMoney = requiredMoney;
this.scalingMultiplier = scalingMultiplier ? scalingMultiplier : 0;
}
meetsRequirement(scene: BattleScene): boolean {
const money = scene.money;
if (!isNullOrUndefined(money) && this?.requiredMoney > 0 && this.requiredMoney > money) {
if (isNullOrUndefined(money)) {
return false;
}
return true;
if (this?.scalingMultiplier > 0) {
this.requiredMoney = scene.getWaveMoneyAmount(this.scalingMultiplier);
}
return !(this?.requiredMoney > 0 && this.requiredMoney > money);
}
getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
return ["money", "₽" + scene.money.toString()];
const value = this?.scalingMultiplier > 0 ? scene.getWaveMoneyAmount(this.scalingMultiplier).toString() : this.requiredMoney.toString();
// Colors money text
return ["money", "@ecCol[MONEY]{₽" + value + "}"];
}
}
@ -399,9 +407,9 @@ export class MoveRequirement extends EncounterPokemonRequirement {
}
getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const includedMoves = this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move.moveId === reqMove).length > 0);
const includedMoves = pokemon.moveset.filter((move) => this.requiredMoves.includes(move.moveId));
if (includedMoves.length > 0) {
return ["move", Moves[includedMoves[0]].replace("_", " ")];
return ["move", includedMoves[0].getName()];
}
return null;
}
@ -552,15 +560,15 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
minNumberOfPokemon:number;
invertQuery:boolean;
constructor(StatusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
constructor(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super();
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
if (StatusEffect instanceof Array) {
this.requiredStatusEffect = StatusEffect;
if (statusEffect instanceof Array) {
this.requiredStatusEffect = statusEffect;
} else {
this.requiredStatusEffect = [];
this.requiredStatusEffect.push(StatusEffect);
this.requiredStatusEffect.push(statusEffect);
}
}
@ -576,16 +584,38 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length > 0);
return partyPokemon.filter((pokemon) => {
return this.requiredStatusEffect.some((statusEffect) => {
if (statusEffect === StatusEffect.NONE) {
// StatusEffect.NONE also checks for null or undefined status
return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === statusEffect;
} else {
return pokemon.status?.effect === statusEffect;
}
});
});
} else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed StatusEffects
return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length === 0);
// return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((statusEffect) => pokemon.status?.effect === statusEffect).length === 0);
return partyPokemon.filter((pokemon) => {
return !this.requiredStatusEffect.some((statusEffect) => {
if (statusEffect === StatusEffect.NONE) {
// StatusEffect.NONE also checks for null or undefined status
return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === statusEffect;
} else {
return pokemon.status?.effect === statusEffect;
}
});
});
}
}
getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const reqStatus = this.requiredStatusEffect.filter((a) => {
pokemon.status?.effect ===(a);
if (a === StatusEffect.NONE) {
return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === a;
}
return pokemon.status?.effect === a;
});
if (reqStatus.length > 0) {
return ["status", StatusEffect[reqStatus[0]]];
@ -863,7 +893,9 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement {
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]);
return partyPokemon.filter((pokemon) => {
return pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1];
});
} else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredHealthRanges
return partyPokemon.filter((pokemon) => pokemon.getHpRatio() < this.requiredHealthRange[0] || pokemon.getHpRatio() > this.requiredHealthRange[1]);

View File

@ -5,10 +5,13 @@ import MysteryEncounterDialogue, {
allMysteryEncounterDialogue
} from "./mystery-encounters/dialogue/mystery-encounter-dialogue";
import MysteryEncounterOption from "./mystery-encounter-option";
import { EncounterPokemonRequirement, EncounterSceneRequirement } from "./mystery-encounter-requirements";
import {
EncounterPokemonRequirement,
EncounterSceneRequirement
} from "./mystery-encounter-requirements";
import * as Utils from "../utils";
import {EnemyPartyConfig} from "#app/data/mystery-encounters/mystery-encounter-utils";
import { PlayerPokemon } from "#app/field/pokemon";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import {isNullOrUndefined} from "../utils";
export enum MysteryEncounterVariant {
@ -167,6 +170,10 @@ export default class MysteryEncounter implements MysteryEncounter {
return sceneReq && secReqs && priReqs;
}
pokemonMeetsPrimaryRequirements?(scene: BattleScene, pokemon: Pokemon) {
return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id));
}
private meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) {
if (this.primaryPokemonRequirements.length === 0) {
const activeMon = scene.getParty().filter(p => p.isActive(true));
@ -263,6 +270,12 @@ export default class MysteryEncounter implements MysteryEncounter {
* For multiple support pokemon in the dialogue token, it will have to be overridden.
*/
populateDialogueTokensFromRequirements?(scene: BattleScene) {
if (this.requirements?.length > 0) {
for (const req of this.requirements) {
const dialogueToken = req.getDialogueToken(scene);
this.setDialogueToken(...dialogueToken);
}
}
if (this.primaryPokemon?.length > 0) {
this.setDialogueToken("primaryName", this.primaryPokemon.name);
for (const req of this.primaryPokemonRequirements) {
@ -281,9 +294,17 @@ export default class MysteryEncounter implements MysteryEncounter {
}
}
}
// Dialogue tokens for options
for (let i = 0; i < this.options.length; i++) {
const opt = this.options[i];
const j = i + 1;
if (opt.requirements?.length > 0) {
for (const req of opt.requirements) {
const dialogueToken = req.getDialogueToken(scene);
this.setDialogueToken("option" + j + this.capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]);
}
}
if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) {
this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.name);
for (const req of opt.primaryPokemonRequirements) {

View File

@ -0,0 +1,120 @@
import BattleScene from "../../battle-scene";
import {
leaveEncounterWithoutBattle,
setCustomEncounterRewards,
} from "#app/data/mystery-encounters/mystery-encounter-utils";
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import {WaveCountRequirement} from "../mystery-encounter-requirements";
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import {modifierTypes} from "#app/modifier/modifier-type";
import {Species} from "#enums/species";
import {randSeedInt} from "#app/utils";
export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncounterBuilder()
.withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE)
.withEncounterTier(MysteryEncounterTier.COMMON)
.withIntroSpriteConfigs([
{
spriteKey: "b2w2_lady",
fileRoot: "mystery-encounters",
hasShadow: true,
x: -20
},
{
spriteKey: Species.FURFROU.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: 30
}
])
// .withHideIntroVisuals(false)
.withSceneRequirement(new WaveCountRequirement([10, 100]))
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Choose TMs
const modifiers = [];
let i = 0;
while (i < 4) {
// 2/2/1 weight on TM rarity
const roll = randSeedInt(5);
if (roll < 2) {
modifiers.push(modifierTypes.TM_COMMON);
} else if (roll < 4) {
modifiers.push(modifierTypes.TM_GREAT);
} else {
modifiers.push(modifierTypes.TM_ULTRA);
}
i++;
}
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false});
leaveEncounterWithoutBattle(scene);
})
.build())
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Choose Vitamins
const modifiers = [];
let i = 0;
while (i < 3) {
// 2/1 weight on base stat booster vs PP Up
const roll = randSeedInt(3);
if (roll === 0) {
modifiers.push(modifierTypes.PP_UP);
} else {
modifiers.push(modifierTypes.BASE_STAT_BOOSTER);
}
i++;
}
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false});
leaveEncounterWithoutBattle(scene);
})
.build())
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Choose X Items
const modifiers = [];
let i = 0;
while (i < 5) {
// 4/1 weight on base stat booster vs Dire Hit
const roll = randSeedInt(5);
if (roll === 0) {
modifiers.push(modifierTypes.DIRE_HIT);
} else {
modifiers.push(modifierTypes.TEMP_STAT_BOOSTER);
}
i++;
}
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false});
leaveEncounterWithoutBattle(scene);
})
.build())
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Choose Pokeballs
const modifiers = [];
let i = 0;
while (i < 4) {
// 10/30/20/5 weight on pokeballs
const roll = randSeedInt(65);
if (roll < 10) {
modifiers.push(modifierTypes.POKEBALL);
} else if (roll < 40) {
modifiers.push(modifierTypes.GREAT_BALL);
} else if (roll < 60) {
modifiers.push(modifierTypes.ULTRA_BALL);
} else {
modifiers.push(modifierTypes.ROGUE_BALL);
}
i++;
}
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false});
leaveEncounterWithoutBattle(scene);
})
.build())
.build();

View File

@ -0,0 +1,36 @@
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
export const DepartmentStoreSaleDialogue: MysteryEncounterDialogue = {
intro: [
{
text: "mysteryEncounter:department_store_sale_intro_message"
},
{
text: "mysteryEncounter:department_store_sale_intro_dialogue",
speaker: "mysteryEncounter:department_store_sale_speaker"
}
],
encounterOptionsDialogue: {
title: "mysteryEncounter:department_store_sale_title",
description: "mysteryEncounter:department_store_sale_description",
query: "mysteryEncounter:department_store_sale_query",
options: [
{
buttonLabel: "mysteryEncounter:department_store_sale_option_1_label",
buttonTooltip: "mysteryEncounter:department_store_sale_option_1_tooltip"
},
{
buttonLabel: "mysteryEncounter:department_store_sale_option_2_label",
buttonTooltip: "mysteryEncounter:department_store_sale_option_2_tooltip"
},
{
buttonLabel: "mysteryEncounter:department_store_sale_option_3_label",
buttonTooltip: "mysteryEncounter:department_store_sale_option_3_tooltip"
},
{
buttonLabel: "mysteryEncounter:department_store_sale_option_4_label",
buttonTooltip: "mysteryEncounter:department_store_sale_option_4_tooltip"
}
]
}
};

View File

@ -5,10 +5,14 @@ import {DarkDealDialogue} from "#app/data/mystery-encounters/dialogue/dark-deal-
import {FightOrFlightDialogue} from "#app/data/mystery-encounters/dialogue/fight-or-flight-dialogue";
import {TrainingSessionDialogue} from "#app/data/mystery-encounters/dialogue/training-session-dialogue";
import { SleepingSnorlaxDialogue } from "./sleeping-snorlax-dialogue";
import {DepartmentStoreSaleDialogue} from "#app/data/mystery-encounters/dialogue/department-store-sale-dialogue";
import {ShadyVitaminDealerDialogue} from "#app/data/mystery-encounters/dialogue/shady-vitamin-dealer";
import {TextStyle} from "#app/ui/text";
export class TextDisplay {
speaker?: TemplateStringsArray | `mysteryEncounter:${string}`;
text: TemplateStringsArray | `mysteryEncounter:${string}`;
style?: TextStyle;
}
export class OptionTextDisplay {
@ -17,6 +21,7 @@ export class OptionTextDisplay {
disabledTooltip?: TemplateStringsArray | `mysteryEncounter:${string}`;
secondOptionPrompt?: TemplateStringsArray | `mysteryEncounter:${string}`;
selected?: TextDisplay[];
style?: TextStyle;
}
export class EncounterOptionsDialogue {
@ -91,4 +96,6 @@ export function initMysteryEncounterDialogue() {
allMysteryEncounterDialogue[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightDialogue;
allMysteryEncounterDialogue[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionDialogue;
allMysteryEncounterDialogue[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxDialogue;
allMysteryEncounterDialogue[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleDialogue;
allMysteryEncounterDialogue[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerDialogue;
}

View File

@ -0,0 +1,42 @@
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
export const ShadyVitaminDealerDialogue: MysteryEncounterDialogue = {
intro: [
{
text: "mysteryEncounter:shady_vitamin_dealer_intro_message"
},
{
text: "mysteryEncounter:shady_vitamin_dealer_intro_dialogue",
speaker: "mysteryEncounter:shady_vitamin_dealer_speaker"
}
],
encounterOptionsDialogue: {
title: "mysteryEncounter:shady_vitamin_dealer_title",
description: "mysteryEncounter:shady_vitamin_dealer_description",
query: "mysteryEncounter:shady_vitamin_dealer_query",
options: [
{
buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_1_label",
buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_1_tooltip",
selected: [
{
text: "mysteryEncounter:shady_vitamin_dealer_option_selected"
},
]
},
{
buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_2_label",
buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_2_tooltip",
selected: [
{
text: "mysteryEncounter:shady_vitamin_dealer_option_selected"
},
]
},
{
buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_3_label",
buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_3_tooltip"
}
]
}
};

View File

@ -9,7 +9,7 @@ import {
} from "#app/data/mystery-encounters/mystery-encounter-utils";
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
import {WaveCountRequirement} from "../mystery-encounter-requirements";
import {MoveRequirement, WaveCountRequirement} from "../mystery-encounter-requirements";
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
import {
getPartyLuckValue,
@ -23,6 +23,18 @@ import {StatChangePhase} from "#app/phases";
import {BattleStat} from "#app/data/battle-stat";
import Pokemon from "#app/field/pokemon";
import {randSeedInt} from "#app/utils";
import {Moves} from "#enums/moves";
import {TextStyle} from "#app/ui/text";
const validMovesForSteal = [
Moves.PLUCK,
Moves.COVET,
Moves.FAKE_OUT,
Moves.THIEF,
Moves.TRICK,
Moves.SWITCHEROO,
Moves.GIGA_DRAIN
];
export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuilder()
.withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT)
@ -70,6 +82,21 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil
}
];
// If player has a stealing move, they succeed automatically
const moveRequirement = new MoveRequirement(validMovesForSteal);
const validPokemon = moveRequirement.queryParty(scene.getParty());
if (validPokemon?.length > 0) {
// Use first valid pokemon to execute the theivery
const pokemon = validPokemon[0];
encounter.setDialogueToken("thiefPokemon", pokemon.name);
encounter.setDialogueToken(...moveRequirement.getDialogueToken(scene, pokemon));
encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_steal_tooltip";
encounter.dialogue.encounterOptionsDialogue.options[1].style = TextStyle.SUMMARY_GREEN;
} else {
encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_tooltip";
encounter.dialogue.encounterOptionsDialogue.options[1].style = null;
}
return true;
})
.withOption(new MysteryEncounterOptionBuilder()
@ -83,9 +110,23 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Pick steal
const encounter = scene.currentBattle.mysteryEncounter;
const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption;
setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false});
// If player has a stealing move, they succeed automatically
const moveRequirement = new MoveRequirement(validMovesForSteal);
const validPokemon = moveRequirement.queryParty(scene.getParty());
if (validPokemon?.length > 0) {
// Use first valid pokemon to execute the theivery
const pokemon = validPokemon[0];
encounter.setDialogueToken("thiefPokemon", pokemon.name);
encounter.setDialogueToken(...moveRequirement.getDialogueToken(scene, pokemon));
await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_steal_result");
leaveEncounterWithoutBattle(scene);
return;
}
const roll = randSeedInt(16);
if (roll > 6) {
// Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%)
@ -101,8 +142,8 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil
} else {
// Steal item (37.5%)
// Display result message then proceed to rewards
await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result")
.then(() => leaveEncounterWithoutBattle(scene));
await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result");
leaveEncounterWithoutBattle(scene);
}
})
.build())

View File

@ -10,8 +10,12 @@ import Trainer, {TrainerVariant} from "../../field/trainer";
import {PokemonExpBoosterModifier} from "#app/modifier/modifier";
import {
CustomModifierSettings,
getModifierPoolForType,
ModifierPoolType,
ModifierType,
ModifierTypeFunc,
ModifierTypeGenerator,
modifierTypes,
PokemonHeldItemModifierType,
regenerateModifierPoolThresholds
} from "#app/modifier/modifier-type";
@ -31,6 +35,7 @@ import {Mode} from "#app/ui/ui";
import {PartyOption, PartyUiMode} from "#app/ui/party-ui-handler";
import {OptionSelectConfig, OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler";
import {WIGHT_INCREMENT_ON_SPAWN_MISS} from "#app/data/mystery-encounters/mystery-encounters";
import {getBBCodeFrag, TextStyle} from "#app/ui/text";
/**
*
@ -162,21 +167,35 @@ export function koPlayerPokemon(pokemon: PlayerPokemon) {
pokemon.updateInfo();
}
export function getTextWithEncounterDialogueTokens(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`): string {
export function getTextWithEncounterDialogueTokensAndColor(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`, primaryStyle: TextStyle = TextStyle.MESSAGE): string {
if (isNullOrUndefined(textKey)) {
return null;
}
let textString: string = i18next.t(textKey);
const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens;
// Apply primary styling before anything else, if it exists
textString = getBBCodeFrag(textString, primaryStyle) + "[/color][/shadow]";
const primaryStyleString = [...textString.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0];
// Apply dialogue tokens
const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens;
if (dialogueTokens) {
dialogueTokens.forEach((value) => {
textString = textString.replace(value[0], value[1]);
});
}
// Set custom colors
// Looks for any pattern like this: @ecCol[SUMMARY_BLUE]{my text to color}
// Resulting in: "my text to color" string with TextStyle.SUMMARY_BLUE
textString = textString.replace(/@ecCol\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => {
return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle]) + "[/color][/shadow]" + primaryStyleString;
});
// Remove extra style block at the end
textString = textString.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, "");
return textString;
}
@ -186,7 +205,7 @@ export function getTextWithEncounterDialogueTokens(scene: BattleScene, textKey:
* @param contentKey
*/
export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): void {
const text: string = getTextWithEncounterDialogueTokens(scene, contentKey);
const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE);
scene.queueMessage(text, null, true);
}
@ -197,7 +216,7 @@ export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateSt
*/
export function showEncounterText(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): Promise<void> {
return new Promise<void>(resolve => {
const text: string = getTextWithEncounterDialogueTokens(scene, contentKey);
const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE);
scene.ui.showText(text, null, () => resolve(), 0, true);
});
}
@ -210,8 +229,8 @@ export function showEncounterText(scene: BattleScene, contentKey: TemplateString
* @param callback
*/
export function showEncounterDialogue(scene: BattleScene, textContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, speakerContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, callback?: Function) {
const text: string = getTextWithEncounterDialogueTokens(scene, textContentKey);
const speaker: string = getTextWithEncounterDialogueTokens(scene, speakerContentKey);
const text: string = getTextWithEncounterDialogueTokensAndColor(scene, textContentKey, TextStyle.MESSAGE);
const speaker: string = getTextWithEncounterDialogueTokensAndColor(scene, speakerContentKey);
scene.ui.showDialogue(text, speaker, null, callback, 0, 0);
}
@ -399,6 +418,50 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
}
}
/**
* Will update player money, and animate change (sound optional)
* @param scene - Battle Scene
* @param changeValue
* @param playSound
*/
export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true) {
scene.money += changeValue;
scene.updateMoneyText();
scene.animateMoneyChanged(false);
if (playSound) {
scene.playSound("buy");
}
}
/**
* Converts modifier bullshit to an actual item
* @param scene - Battle Scene
* @param modifier
* @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc.
*/
export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType {
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier);
let result: ModifierType = modifierTypes[modifierId]?.();
// Gets tier of item by checking player item pool
const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER);
Object.keys(modifierPool).every(modifierTier => {
const modType = modifierPool[modifierTier].find(m => {
if (m.modifierType.id === modifierId) {
return m;
}
});
if (modType) {
result = modType.modifierType;
return false;
}
return true;
});
result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
return result;
}
/**
* Will initialize reward phases to follow the mystery encounter
* Can have shop displayed or skipped
@ -439,8 +502,9 @@ export function setCustomEncounterRewards(scene: BattleScene, customShopRewards?
* @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen
* If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object
* @param onPokemonNotSelected - Any logic that needs to be performed if no Pokemon is chosen
* @param selectablePokemonFilter
*/
export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void): Promise<boolean> {
export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise<boolean> {
return new Promise(resolve => {
// Open party screen to choose pokemon to train
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => {
@ -493,7 +557,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
if (!textPromptKey) {
displayOptions();
} else {
const secondOptionSelectPrompt = getTextWithEncounterDialogueTokens(scene, textPromptKey);
const secondOptionSelectPrompt = getTextWithEncounterDialogueTokensAndColor(scene, textPromptKey, TextStyle.MESSAGE);
scene.ui.showText(secondOptionSelectPrompt, null, displayOptions, null, true);
}
});
@ -506,7 +570,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
resolve(false);
});
}
});
}, selectablePokemonFilter);
});
}

View File

@ -7,6 +7,8 @@ import {TrainingSessionEncounter} from "#app/data/mystery-encounters/training-se
import { Biome } from "#app/enums/biome";
import { SleepingSnorlaxEncounter } from "./sleeping-snorlax";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import {DepartmentStoreSaleEncounter} from "#app/data/mystery-encounters/department-store-sale";
import {ShadyVitaminDealerEncounter} from "#app/data/mystery-encounters/shady-vitamin-dealer";
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -19,18 +21,20 @@ export const allMysteryEncounters : {[encounterType:string]: MysteryEncounter} =
// To enable an encounter in all biomes, do not add to this map
export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.TOWN, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.PLAINS,[
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.GRASS, [
MysteryEncounterType.SLEEPING_SNORLAX
MysteryEncounterType.SLEEPING_SNORLAX,
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.TALL_GRASS, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.METROPOLIS, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.FOREST, [
MysteryEncounterType.SLEEPING_SNORLAX
@ -43,7 +47,7 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
]],
[Biome.BEACH, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.LAKE, [
@ -67,10 +71,10 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
]],
[Biome.MEADOW, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.POWER_PLANT, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.VOLCANO, [
@ -82,7 +86,7 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
]],
[Biome.FACTORY, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.RUINS, [
@ -97,7 +101,7 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
]],
[Biome.CONSTRUCTION_SITE, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.JUNGLE, [
@ -109,7 +113,7 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
]],
[Biome.SLUM, [
MysteryEncounterType.DEPARTMENT_STORE_SALE
]],
[Biome.SNOWY_FOREST, [
@ -131,6 +135,8 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter;
allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter;
allMysteryEncounters[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxEncounter;
allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter;
allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter;
// Append encounters that can occur in any biome to biome map
const anyBiomeEncounters: MysteryEncounterType[] = Object.keys(MysteryEncounterType).filter(e => !isNaN(Number(e))).map(k => Number(k) as MysteryEncounterType);

View File

@ -0,0 +1,148 @@
import BattleScene from "../../battle-scene";
import {
generateModifierType,
leaveEncounterWithoutBattle,
queueEncounterMessage,
selectPokemonForOption,
setCustomEncounterRewards,
updatePlayerMoney,
} from "#app/data/mystery-encounters/mystery-encounter-utils";
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
import {
HealthRatioRequirement,
MoneyRequirement,
StatusEffectRequirement,
WaveCountRequirement
} from "../mystery-encounter-requirements";
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
import {modifierTypes} from "#app/modifier/modifier-type";
import {Species} from "#enums/species";
import {randSeedInt} from "#app/utils";
import Pokemon, {PlayerPokemon} from "#app/field/pokemon";
import {StatusEffect} from "#app/data/status-effect";
export const ShadyVitaminDealerEncounter: MysteryEncounter = new MysteryEncounterBuilder()
.withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER)
.withEncounterTier(MysteryEncounterTier.COMMON)
.withIntroSpriteConfigs([
{
spriteKey: Species.KROOKODILE.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: 10,
y: -1
},
{
spriteKey: "b2w2_veteran_m",
fileRoot: "mystery-encounters",
hasShadow: true,
x: -10,
y: 2
}
])
.withSceneRequirement(new WaveCountRequirement([10, 180]))
.withPrimaryPokemonRequirement(new StatusEffectRequirement([StatusEffect.NONE])) // Pokemon must not have status
.withPrimaryPokemonRequirement(new HealthRatioRequirement([0.34, 1])) // Pokemon must have above 1/3rd HP
.withOption(new MysteryEncounterOptionBuilder()
.withSceneRequirement(new MoneyRequirement(0, 2)) // Wave scaling multiplier of 2 for cost
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Update money
updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens
const modifiers = [
generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER),
generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = {
chosenPokemon: pokemon,
modifiers: modifiers
};
};
// Only Pokemon that can gain benefits are unfainted with no status
const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected
const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) {
return "Pokémon must be healthy enough.";
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
// Choose Cheap Option
const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers;
for (const modType of modifiers) {
const modifier = modType.newModifier(chosenPokemon);
await scene.addModifier(modifier, true, false, false, true);
}
scene.updateModifiers(true);
leaveEncounterWithoutBattle(scene);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Damage and status applied after dealer leaves (to make thematic sense)
const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon;
// Pokemon takes 1/3 max HP damage
const damage = Math.round(chosenPokemon.getMaxHp() / 3);
chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0);
// Roll for poison (80%)
if (randSeedInt(10) < 10) {
if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) {
// Toxic applied
queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_bad_poison");
} else {
// Pokemon immune or something else prevents status
queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only");
}
} else {
queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only");
}
chosenPokemon.updateInfo();
})
.build())
.withOption(new MysteryEncounterOptionBuilder()
.withSceneRequirement(new MoneyRequirement(0, 5)) // Wave scaling multiplier of 2 for cost
.withOptionPhase(async (scene: BattleScene) => {
// Choose Expensive Option
const modifiers = [];
let i = 0;
while (i < 3) {
// 2/1 weight on base stat booster vs PP Up
const roll = randSeedInt(3);
if (roll === 0) {
modifiers.push(modifierTypes.PP_UP);
} else {
}
i++;
}
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false});
leaveEncounterWithoutBattle(scene);
})
.build())
.withOption(new MysteryEncounterOptionBuilder()
.withOptionPhase(async (scene: BattleScene) => {
// Leave encounter with no rewards or exp
leaveEncounterWithoutBattle(scene, true);
return true;
})
.build())
.build();

View File

@ -1,7 +1,7 @@
import BattleScene from "../../battle-scene";
import {
EnemyPartyConfig,
EnemyPokemonConfig,
EnemyPokemonConfig, generateModifierType,
initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, queueEncounterMessage,
setCustomEncounterRewards
@ -12,7 +12,6 @@ import {MysteryEncounterType} from "#enums/mystery-encounter-type";
import {MoveRequirement, WaveCountRequirement} from "../mystery-encounter-requirements";
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
import {
ModifierTypeGenerator,
ModifierTypeOption,
modifierTypes
} from "#app/modifier/modifier-type";
@ -76,7 +75,8 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBu
const p = instance.primaryPokemon;
p.status = new Status(StatusEffect.SLEEP, 0, 3);
p.updateInfo(true);
const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]);
// const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]);
const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]);
setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false});
queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result");

View File

@ -1,7 +1,7 @@
import BattleScene from "../../battle-scene";
import {
EnemyPartyConfig,
getTextWithEncounterDialogueTokens,
getTextWithEncounterDialogueTokensAndColor,
initBattleWithEnemyConfig,
selectPokemonForOption,
setCustomEncounterRewards
@ -128,7 +128,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu
scene.addModifier(mod, true, false, false, true);
}
scene.updateModifiers(true);
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true);
scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true);
};
setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase);
@ -174,7 +174,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu
scene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => {
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true);
scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true);
// Add the pokemon back to party with Nature change
playerPokemon.setNature(encounter.misc.chosenNature);
scene.gameData.setPokemonCaught(playerPokemon, false);
@ -237,7 +237,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu
scene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => {
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true);
scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true);
// Add the pokemon back to party with ability change
const abilityIndex = encounter.misc.abilityIndex;
if (!!playerPokemon.getFusionSpeciesForm()) {

View File

@ -4,5 +4,7 @@ export enum MysteryEncounterType {
DARK_DEAL,
FIGHT_OR_FLIGHT,
SLEEPING_SNORLAX,
TRAINING_SESSION
TRAINING_SESSION,
DEPARTMENT_STORE_SALE,
SHADY_VITAMIN_DEALER
}

View File

@ -79,12 +79,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
// Sprite offset from origin
if (config.x || config.y) {
if (config.x) {
sprite.x = origin + config.x;
tintSprite.x = origin + config.x;
sprite.setPosition(origin + config.x, sprite.y);
tintSprite.setPosition(origin + config.x, tintSprite.y);
}
if (config.y) {
sprite.y = origin + config.y;
tintSprite.y = origin + config.y;
sprite.setPosition(sprite.x, config.y);
tintSprite.setPosition(tintSprite.x, config.y);
}
} else {
// Single sprite

View File

@ -1,17 +1,28 @@
import {SimpleTranslationEntries} from "#app/interfaces/locales";
/**
* Patterns that can be used:
* '$' will be treated as a new line for Message and Dialogue strings
* '@d{<number>}' will add a time delay to text animation for Message and Dialogue strings
*
* '@ec{<token>}' will auto-inject the matching token value for the specified Encounter
*
* '@ecCol[<TextStyle>]{<text>}' will auto-color the given text to a specified TextStyle (e.g. TextStyle.SUMMARY_GREEN)
*
* Any '(+)' or '(-)' type of tooltip will auto-color to green/blue respectively. THIS ONLY OCCURS FOR OPTION TOOLTIPS, NOWHERE ELSE
* Other types of '(...)' tooltips will have to specify the text color manually by using '@ecCol[SUMMARY_GREEN]{<text>}' pattern
*/
export const mysteryEncounter: SimpleTranslationEntries = {
// DO NOT REMOVE
"unit_test_dialogue": "@ec{test}@ec{test} @ec{test@ec{test}} @ec{test1} @ec{test\} @ec{test\\} @ec{test\\\} {test}",
// Mysterious Encounters -- Common Tier
// Mystery Encounters -- Common Tier
"mysterious_chest_intro_message": "You found...@d{32} a chest?",
"mysterious_chest_title": "The Mysterious Chest",
"mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?",
"mysterious_chest_query": "Will you open it?",
"mysterious_chest_option_1_label": "Open it",
"mysterious_chest_option_1_tooltip": "(35%) Something terrible\n(40%) Okay Rewards\n(20%) Good Rewards\n(4%) Great Rewards\n(1%) Amazing Rewards",
"mysterious_chest_option_1_tooltip": "@ecCol[SUMMARY_BLUE]{(35%) Something terrible}\n@ecCol[SUMMARY_GREEN]{(40%) Okay Rewards}\n@ecCol[SUMMARY_GREEN]{(20%) Good Rewards}\n@ecCol[SUMMARY_GREEN]{(4%) Great Rewards}\n@ecCol[SUMMARY_GREEN]{(1%) Amazing Rewards}",
"mysterious_chest_option_2_label": "It's too risky, leave",
"mysterious_chest_option_2_tooltip": "(-) No Rewards",
"mysterious_chest_option_1_selected_message": "You open the chest to find...",
@ -27,36 +38,82 @@ export const mysteryEncounter: SimpleTranslationEntries = {
"fight_or_flight_title": "Fight or Flight",
"fight_or_flight_description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.",
"fight_or_flight_query": "What will you do?",
"fight_or_flight_option_1_label": "Battle it",
"fight_or_flight_option_1_tooltip": "(+) Hard Battle\n(+) New Item",
"fight_or_flight_option_2_label": "Sneak around",
"fight_or_flight_option_2_tooltip": "(35%) Steal Item\n(65%) Harder Battle",
"fight_or_flight_option_1_label": "Battle the Pokémon",
"fight_or_flight_option_1_tooltip": "(-) Hard Battle\n(+) New Item",
"fight_or_flight_option_2_label": "Steal the item",
"fight_or_flight_option_2_tooltip": "@ecCol[SUMMARY_GREEN]{(35%) Steal Item}\n@ecCol[SUMMARY_BLUE]{(65%) Harder Battle}",
"fight_or_flight_option_2_steal_tooltip": "@ecCol[SUMMARY_GREEN]{(?) Use a Pokémon Move}",
"fight_or_flight_option_3_label": "Leave",
"fight_or_flight_option_3_tooltip": "(-) No Rewards",
"fight_or_flight_option_1_selected_message": "You approach the\nPokémon without fear.",
"fight_or_flight_option_2_good_result": `.@d{32}.@d{32}.@d{32}
$You manage to sneak your way\npast and grab the item!`,
"fight_or_flight_option_2_steal_result": `.@d{32}.@d{32}.@d{32}
$Your @ec{thiefPokemon} helps you out and uses @ec{move}!
$ You nabbed the item!`,
"fight_or_flight_option_2_bad_result": `.@d{32}.@d{32}.@d{32}
$The Pokémon catches you\nas you try to sneak around!`,
"fight_or_flight_boss_enraged": "The opposing @ec{enemyPokemon} has become enraged!",
"fight_or_flight_option_3_selected": "You leave the strong Pokémon\nwith its prize and continue on.",
// Mysterious Encounters -- Uncommon Tier
"department_store_sale_intro_message": "It's a lady with a ton of shopping bags.",
"department_store_sale_speaker": "Shopper",
"department_store_sale_intro_dialogue": `Hello! Are you here for\nthe amazing sales too?
$There's a special coupon that you can\nredeem for a free item during the sale!
$I have an extra one. Here you go!`,
"department_store_sale_title": "Department Store Sale",
"department_store_sale_description": "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!",
"department_store_sale_query": "Which counter will you go to?",
"department_store_sale_option_1_label": "TM Counter",
"department_store_sale_option_1_tooltip": "(+) TM Shop",
"department_store_sale_option_2_label": "Vitamin Counter",
"department_store_sale_option_2_tooltip": "(+) Vitamin Shop",
"department_store_sale_option_3_label": "Battle Item Counter",
"department_store_sale_option_3_tooltip": "(+) X Item Shop",
"department_store_sale_option_4_label": "Pokéball Counter",
"department_store_sale_option_4_tooltip": "(+) Pokéball Shop",
"department_store_sale_outro": "What a deal! You should shop there more often.",
"shady_vitamin_dealer_intro_message": "A man in a dark coat approaches you.",
"shady_vitamin_dealer_speaker": "Shady Salesman",
"shady_vitamin_dealer_intro_dialogue": `.@d{16}.@d{16}.@d{16}
$I've got the goods if you've got the money.
$Make sure your Pokémon can handle it though.`,
"shady_vitamin_dealer_title": "The Vitamin Dealer",
"shady_vitamin_dealer_description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.",
"shady_vitamin_dealer_query": "Which deal will choose?",
"shady_vitamin_dealer_option_1_label": "The Cheap Deal",
"shady_vitamin_dealer_option_1_tooltip": "(-) Pay @ec{option1Money}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins",
"shady_vitamin_dealer_option_2_label": "The Pricey Deal",
"shady_vitamin_dealer_option_2_tooltip": "(-) Pay @ec{option2Money}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins",
"shady_vitamin_dealer_option_selected": `The man hands you two bottles and quickly disappears.
$@ec{selectedPokemon} gained @ec{boost1} and @ec{boost2} boosts!`,
"shady_vitamin_dealer_damage_only": `But the medicine had some side effects!
$Your @ec{selectedPokemon} takes some damage...`,
"shady_vitamin_dealer_bad_poison": `But the medicine had some side effects!
$Your @ec{selectedPokemon} takes some damage\nand becomes badly poisoned...`,
"shady_vitamin_dealer_poison": `But the medicine had some side effects!
$Your @ec{selectedPokemon} becomes poisoned...`,
"shady_vitamin_dealer_option_3_label": "Leave",
"shady_vitamin_dealer_option_3_tooltip": "(-) No Rewards",
"shady_vitamin_dealer_outro_good": "Looks like there were no side-effects this time.",
// Mystery Encounters -- Uncommon Tier
"mysterious_challengers_intro_message": "Mysterious challengers have appeared!",
"mysterious_challengers_title": "Mysterious Challengers",
"mysterious_challengers_description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?",
"mysterious_challengers_query": "Who will you battle?",
"mysterious_challengers_option_1_label": "A clever, mindful foe",
"mysterious_challengers_option_1_tooltip": "(+) Standard Battle\n(+) Move Item Rewards",
"mysterious_challengers_option_1_tooltip": "(-) Standard Battle\n(+) Move Item Rewards",
"mysterious_challengers_option_2_label": "A strong foe",
"mysterious_challengers_option_2_tooltip": "(+) Hard Battle\n(+) Good Rewards",
"mysterious_challengers_option_2_tooltip": "(-) Hard Battle\n(+) Good Rewards",
"mysterious_challengers_option_3_label": "The mightiest foe",
"mysterious_challengers_option_3_tooltip": "(+) Brutal Battle\n(+) Great Rewards",
"mysterious_challengers_option_3_tooltip": "(-) Brutal Battle\n(+) Great Rewards",
"mysterious_challengers_option_selected_message": "The trainer steps forward...",
"mysterious_challengers_outro_win": "The mysterious challenger was defeated!",
// Mysterious Encounters -- Rare Tier
// Mystery Encounters -- Rare Tier
"training_session_intro_message": "You've come across some\ntraining tools and supplies.",
"training_session_title": "Training Session",
"training_session_description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.",
@ -78,7 +135,7 @@ export const mysteryEncounter: SimpleTranslationEntries = {
$Its ability was changed to @ec{ability}!`,
"training_session_outro_win": "That was a successful training session!",
// Mysterious Encounters -- Super Rare Tier
// Mystery Encounters -- Super Rare Tier
"dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...",
"dark_deal_speaker": "Shady Guy",
@ -108,9 +165,9 @@ export const mysteryEncounter: SimpleTranslationEntries = {
"sleeping_snorlax_description": "You could attack it to try and get it to move, or simply wait for it to wake up.",
"sleeping_snorlax_query": "What will you do?",
"sleeping_snorlax_option_1_label": "Fight it",
"sleeping_snorlax_option_1_tooltip": "(+) Fight Sleeping Snorlax",
"sleeping_snorlax_option_1_tooltip": "(-) Fight Sleeping Snorlax",
"sleeping_snorlax_option_2_label": "Wait for it to move",
"sleeping_snorlax_option_2_tooltip": "(75%) Wait a short time\n(25%) Wait a long time",
"sleeping_snorlax_option_2_tooltip": "@ecCol[SUMMARY_BLUE]{(75%) Wait a short time}\n@ecCol[SUMMARY_BLUE]{(25%) Wait a long time}",
"sleeping_snorlax_option_3_label": "Steal",
"sleeping_snorlax_option_3_tooltip": "(+) Leftovers",
"sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this",

View File

@ -118,9 +118,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
*/
// 1 to 256, set to null to ignore
export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null;
export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256;
export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null;
export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null;
export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.SHADY_VITAMIN_DEALER;
/**
* MODIFIER / ITEM OVERRIDES

View File

@ -3,7 +3,7 @@ import BattleScene from "../battle-scene";
import { Phase } from "../phase";
import { Mode } from "../ui/ui";
import {
getTextWithEncounterDialogueTokens
getTextWithEncounterDialogueTokensAndColor
} from "../data/mystery-encounters/mystery-encounter-utils";
import { CheckSwitchPhase, NewBattlePhase, PostSummonPhase, ReturnPhase, ScanIvsPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases";
import MysteryEncounterOption from "../data/mystery-encounter-option";
@ -89,9 +89,9 @@ export class MysteryEncounterPhase extends Phase {
const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue;
const dialogue = selectedDialogue[i];
let title: string = null;
const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text);
const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text);
if (dialogue.speaker) {
title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker);
title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker);
}
if (title) {
@ -451,9 +451,9 @@ export class PostMysteryEncounterPhase extends Phase {
const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue;
const dialogue = outroDialogue[i];
let title: string = null;
const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text);
const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text);
if (dialogue.speaker) {
title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker);
title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker);
}
this.scene.ui.setMode(Mode.MESSAGE);

View File

@ -3,7 +3,7 @@ import GameManager from "#app/test/utils/gameManager";
import Phaser from "phaser";
import {
getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon,
getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getTextWithEncounterDialogueTokens,
getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getTextWithEncounterDialogueTokensAndColor,
koPlayerPokemon, queueEncounterMessage, showEncounterDialogue, showEncounterText,
} from "#app/data/mystery-encounters/mystery-encounter-utils";
import {initSceneWithoutEncounterPhase} from "#test/utils/gameManagerUtils";
@ -272,12 +272,12 @@ describe("Mystery Encounter Utils", () => {
});
describe("getTextWithEncounterDialogueTokens", () => {
it("injects dialogue tokens", () => {
it("injects dialogue tokens and color styling", () => {
scene.currentBattle.mysteryEncounter = new MysteryEncounter(null);
scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value");
const result = getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:unit_test_dialogue");
expect(result).toEqual("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}");
const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue");
expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]");
});
it("can perform nested dialogue token injection", () => {
@ -285,8 +285,8 @@ describe("Mystery Encounter Utils", () => {
scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value");
scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new");
const result = getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:unit_test_dialogue");
expect(result).toEqual("valuevalue new @ec{test1} value @ec{test\\} @ec{test\\} {test}");
const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue");
expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue new @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]");
});
});
@ -298,7 +298,7 @@ describe("Mystery Encounter Utils", () => {
const phaseSpy = vi.spyOn(game.scene, "unshiftPhase");
queueEncounterMessage(scene, "mysteryEncounter:unit_test_dialogue");
expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, true);
expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, true);
expect(phaseSpy).toHaveBeenCalledWith(expect.any(MessagePhase));
});
});
@ -310,7 +310,7 @@ describe("Mystery Encounter Utils", () => {
const spy = vi.spyOn(game.scene.ui, "showText");
showEncounterText(scene, "mysteryEncounter:unit_test_dialogue");
expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, expect.any(Function), 0, true);
expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, expect.any(Function), 0, true);
});
});
@ -321,7 +321,7 @@ describe("Mystery Encounter Utils", () => {
const spy = vi.spyOn(game.scene.ui, "showDialogue");
showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue");
expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", "valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, undefined, 0, 0);
expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", "[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, undefined, 0, 0);
});
});

View File

@ -84,7 +84,7 @@ describe("Mystery Encounter Phases", () => {
expect(messageSpy).toHaveBeenCalledTimes(2);
expect(dialogueSpy).toHaveBeenCalledWith("What's this?", "???", null, expect.any(Function));
expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 750, true);
expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 750, true);
expect(messageSpy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]The trainer steps forward...[/color][/shadow]", null, expect.any(Function), 750, true);
});
});

View File

@ -1,16 +1,16 @@
import BattleScene from "../battle-scene";
import { addTextObject, TextStyle } from "./text";
import { Mode } from "./ui";
import {addBBCodeTextObject, getBBCodeFrag, TextStyle} from "./text";
import {Mode} from "./ui";
import UiHandler from "./ui-handler";
import { Button } from "#enums/buttons";
import { addWindow, WindowVariant } from "./ui-theme";
import i18next from "i18next";
import { MysteryEncounterPhase } from "../phases/mystery-encounter-phase";
import { PartyUiMode } from "./party-ui-handler";
import {Button} from "#enums/buttons";
import {addWindow, WindowVariant} from "./ui-theme";
import {MysteryEncounterPhase} from "../phases/mystery-encounter-phase";
import {PartyUiMode} from "./party-ui-handler";
import MysteryEncounterOption from "../data/mystery-encounter-option";
import * as Utils from "../utils";
import { getPokeballAtlasKey } from "../data/pokeball";
import {isNullOrUndefined} from "../utils";
import {getPokeballAtlasKey} from "../data/pokeball";
import {getTextWithEncounterDialogueTokensAndColor} from "#app/data/mystery-encounters/mystery-encounter-utils";
export default class MysteryEncounterUiHandler extends UiHandler {
private cursorContainer: Phaser.GameObjects.Container;
@ -298,9 +298,9 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.filteredEncounterOptions = mysteryEncounter.options;
this.optionsMeetsReqs = [];
const titleText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.title);
const descriptionText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.description);
const queryText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.query);
const titleText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE);
const descriptionText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.description, TextStyle.TOOLTIP_CONTENT);
const queryText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT);
// Clear options container (except cursor)
this.optionsContainer.removeAll();
@ -310,16 +310,17 @@ export default class MysteryEncounterUiHandler extends UiHandler {
let optionText;
switch (this.filteredEncounterOptions.length) {
case 2:
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
break;
case 3:
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
break;
case 4:
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
break;
}
const text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[i].buttonLabel);
const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[i];
const text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonLabel, option.style ? option.style : TextStyle.WINDOW);
if (text) {
optionText.setText(text);
}
@ -336,11 +337,11 @@ export default class MysteryEncounterUiHandler extends UiHandler {
}
// View Party Button
const viewPartyText = addTextObject(this.scene, 256, -24, "View Party", TextStyle.PARTY);
const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag("View Party", TextStyle.PARTY), TextStyle.PARTY);
this.optionsContainer.add(viewPartyText);
// Description Window
const titleTextObject = addTextObject(this.scene, 0, 0, titleText, TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 });
const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText, TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 });
this.descriptionContainer.add(titleTextObject);
titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5);
@ -348,7 +349,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number);
this.rarityBall.setTexture("pb", ballType);
const descriptionTextObject = addTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
// Sets up the mask that hides the description text to give an illusion of scrolling
const descriptionTextMaskRect = this.scene.make.graphics({});
@ -382,8 +383,9 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.descriptionContainer.add(descriptionTextObject);
const queryTextObject = addTextObject(this.scene, 65 - (queryText.length), 90, queryText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
const queryTextObject = addBBCodeTextObject(this.scene, 0, 0, queryText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
this.descriptionContainer.add(queryTextObject);
queryTextObject.setPosition(75 - queryTextObject.displayWidth / 2, 90);
// Slide in description container
if (slideInDescription) {
@ -412,15 +414,20 @@ export default class MysteryEncounterUiHandler extends UiHandler {
const mysteryEncounter = this.scene.currentBattle.mysteryEncounter;
let text;
if (!this.optionsMeetsReqs[cursor] && mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip) {
text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip);
const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor];
if (!this.optionsMeetsReqs[cursor] && option.disabledTooltip) {
text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.disabledTooltip, TextStyle.TOOLTIP_CONTENT);
} else {
text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].buttonTooltip);
text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonTooltip, TextStyle.TOOLTIP_CONTENT);
}
// Auto-color options green/blue for good/bad by looking for (+)/(-)
const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0];
text = text.replace(/(\([^\(]*\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString);
text = text.replace(/(\([^\(]*\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString);
if (text) {
const tooltipTextObject = addTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" });
const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" });
this.tooltipContainer.add(tooltipTextObject);
// Sets up the mask that hides the description text to give an illusion of scrolling