Compare commits
No commits in common. "e40594e107ceffaf769e51c50fd4fd15628da9ed" and "8ffe8348bb883e71f39e892e0da82d7eca94bcf4" have entirely different histories.
e40594e107
...
8ffe8348bb
16
.env
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
DISCORD_TOKEN=MTUxMDk1OTgxNDYyMzEwNTA0NA.GNY7A9.Boq4MruKRqvo1UZ5JmsCkYN7q1xCTNKuqyh1oA
|
||||||
|
POLL_CHANNEL_ID=1511006387293917355
|
||||||
|
RESULTS_CHANNEL_ID=1511006410627088544
|
||||||
|
SCORE_CHANNEL_ID=1511006435079884991
|
||||||
|
CLIENT_ID=1510959814623105044
|
||||||
|
GUILD_ID=1511006171681652858
|
||||||
|
EPHEMERAL_DELETE_MS=0
|
||||||
|
POLL_EPHEMERAL_ENABLED=false # voting messages (disabled during testing)
|
||||||
|
COMMAND_EPHEMERAL_ENABLED=true # command outputs (always on)
|
||||||
|
AUTO_VOTE_ON_CONFLICT_SWITCH=true
|
||||||
|
IMPERSONATE_RESET_ON_POLL=false
|
||||||
|
IMPERSONATE_INDICATOR=true
|
||||||
|
RECLAIM_NOTIFY_BORROWER=true
|
||||||
|
|
||||||
|
# Emoji upload servers
|
||||||
|
EMOJI_DONOR_GUILDS=1511903882224336926,1511904145810915449
|
||||||
6
.gitignore
vendored
|
|
@ -1,8 +1,6 @@
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
# Environment variables — never commit these
|
# Environment variables — never commit these
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|
@ -10,7 +8,6 @@ docker-compose.yml
|
||||||
data/config.json
|
data/config.json
|
||||||
data/characters.json
|
data/characters.json
|
||||||
data/accounts.json
|
data/accounts.json
|
||||||
data/poll-state.json
|
|
||||||
data/usermap.json
|
data/usermap.json
|
||||||
data/wrank.json
|
data/wrank.json
|
||||||
data/bringer.json
|
data/bringer.json
|
||||||
|
|
@ -20,9 +17,6 @@ data/tg-history/
|
||||||
# Emoji data
|
# Emoji data
|
||||||
emoji-uploads/
|
emoji-uploads/
|
||||||
|
|
||||||
# Tests
|
|
||||||
tests/
|
|
||||||
|
|
||||||
# Keep the data directory structure but not the contents
|
# Keep the data directory structure but not the contents
|
||||||
!data/.gitkeep
|
!data/.gitkeep
|
||||||
!data/tg-history/.gitkeep
|
!data/tg-history/.gitkeep
|
||||||
|
|
|
||||||
1
data/accounts.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
5
data/bringer.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"currentWeek": "",
|
||||||
|
"capella": null,
|
||||||
|
"procyon": null
|
||||||
|
}
|
||||||
113
data/characters.json
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
{
|
||||||
|
"flash": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "«Flash»",
|
||||||
|
"class": "FB",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Procyon",
|
||||||
|
"active": false,
|
||||||
|
"sharedWith": [
|
||||||
|
"invicjusz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Procyon",
|
||||||
|
"active": true,
|
||||||
|
"sharedWith": [
|
||||||
|
"invicjusz"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"zephyr": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "XefronYokuda",
|
||||||
|
"class": "FA",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dey": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "«Deystroyer»",
|
||||||
|
"class": "BL",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": true,
|
||||||
|
"sharedWith": [
|
||||||
|
"flash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keira": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "«Keira»",
|
||||||
|
"class": "WI",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ayana": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "«MonkeyHunter»",
|
||||||
|
"class": "DM",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Procyon",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"invicjusz": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "ElementalEnchant",
|
||||||
|
"class": "FB",
|
||||||
|
"level": 76,
|
||||||
|
"nation": "Procyon",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marin": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "iMarieLaveau",
|
||||||
|
"class": "DM",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sean": {
|
||||||
|
"characters": [
|
||||||
|
{
|
||||||
|
"name": "»No.1«",
|
||||||
|
"class": "FB",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "«No.1»",
|
||||||
|
"class": "GL",
|
||||||
|
"level": 79,
|
||||||
|
"nation": "Capella",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
data/config.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"showLevelInMessages": true,
|
||||||
|
"showClassInMessages": true
|
||||||
|
}
|
||||||
26
data/poll-state.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"messageId": "1511906795667456040",
|
||||||
|
"slot": 20,
|
||||||
|
"yes": [
|
||||||
|
[
|
||||||
|
"164487045052497920",
|
||||||
|
{
|
||||||
|
"userKey": "flash",
|
||||||
|
"displayName": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"characterClass": "WI",
|
||||||
|
"characterLevel": 79,
|
||||||
|
"characterNation": "Procyon",
|
||||||
|
"discordId": "164487045052497920",
|
||||||
|
"votedAt": "03:53",
|
||||||
|
"previousNoAt": "03:53",
|
||||||
|
"publicMessage": "Flash? Flash? Flash!!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"no": [],
|
||||||
|
"locked": false,
|
||||||
|
"confirmed": null
|
||||||
|
}
|
||||||
|
]
|
||||||
29
data/tg-history/2026-06-01-00.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 0,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 2000,
|
||||||
|
"submittedAt": "2026-06-01T03:22:25.475Z",
|
||||||
|
"slot": 0,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data/tg-history/2026-06-01-02.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 2,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 100,
|
||||||
|
"submittedAt": "2026-06-01T03:22:43.115Z",
|
||||||
|
"slot": 2,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data/tg-history/2026-06-01-04.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 4,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 100,
|
||||||
|
"submittedAt": "2026-06-01T03:22:48.373Z",
|
||||||
|
"slot": 4,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data/tg-history/2026-06-01-06.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 6,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 100,
|
||||||
|
"submittedAt": "2026-06-01T03:22:54.521Z",
|
||||||
|
"slot": 6,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data/tg-history/2026-06-01-08.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 8,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 100,
|
||||||
|
"submittedAt": "2026-06-01T03:23:03.650Z",
|
||||||
|
"slot": 8,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
data/tg-history/2026-06-01-20.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"slot": 20,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "invicjusz",
|
||||||
|
"characterName": "ElementalEnchant",
|
||||||
|
"class": "FB",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 5000,
|
||||||
|
"submittedAt": "2026-06-01T03:19:12.073Z",
|
||||||
|
"slot": 20,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "«Flash»",
|
||||||
|
"class": "FB",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 2000,
|
||||||
|
"submittedAt": "2026-06-01T22:07:39.907Z",
|
||||||
|
"slot": 20,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data/tg-history/2026-06-01-22.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"slot": 22,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"confirmed": false,
|
||||||
|
"nationKD": {
|
||||||
|
"source": "Procyon",
|
||||||
|
"capella": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
},
|
||||||
|
"procyon": {
|
||||||
|
"k": 0,
|
||||||
|
"d": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"usermapKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"pts": 1000,
|
||||||
|
"submittedAt": "2026-06-01T22:05:28.186Z",
|
||||||
|
"slot": 22,
|
||||||
|
"date": "2026-06-01",
|
||||||
|
"submittedByOfficer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
data/usermap.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"nunoflashy": {
|
||||||
|
"file": "flash",
|
||||||
|
"aliases": ["Flash", "El Flasho"]
|
||||||
|
},
|
||||||
|
"staki91": "dey",
|
||||||
|
"invicjusz": "invicjusz",
|
||||||
|
"mrsean.": "sean",
|
||||||
|
"ibenni": "ayana",
|
||||||
|
"zephyr_74135": "zephyr",
|
||||||
|
"eat.jim.sleep": "keira",
|
||||||
|
"mar1n1987": "marin"
|
||||||
|
}
|
||||||
114
data/wrank.json
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"2026-W23": {
|
||||||
|
"weekKey": "2026-W23",
|
||||||
|
"entries": {
|
||||||
|
"capella": [
|
||||||
|
{
|
||||||
|
"userKey": "zephyr",
|
||||||
|
"characterName": "XefronYokuda",
|
||||||
|
"class": "FA",
|
||||||
|
"nation": "Capella",
|
||||||
|
"weeklyPoints": 1415,
|
||||||
|
"tgCount": 2,
|
||||||
|
"currentRank": 4,
|
||||||
|
"previousRank": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userKey": "dey",
|
||||||
|
"characterName": "«Deystroyer»",
|
||||||
|
"class": "BL",
|
||||||
|
"nation": "Capella",
|
||||||
|
"weeklyPoints": 3640,
|
||||||
|
"tgCount": 2,
|
||||||
|
"currentRank": 2,
|
||||||
|
"previousRank": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userKey": "keira",
|
||||||
|
"characterName": "«Keira»",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Capella",
|
||||||
|
"weeklyPoints": 4000,
|
||||||
|
"tgCount": 1,
|
||||||
|
"currentRank": 1,
|
||||||
|
"previousRank": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userKey": "sean",
|
||||||
|
"characterName": "»No.1«",
|
||||||
|
"class": "FB",
|
||||||
|
"nation": "Capella",
|
||||||
|
"weeklyPoints": 1666,
|
||||||
|
"tgCount": 1,
|
||||||
|
"currentRank": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"procyon": [
|
||||||
|
{
|
||||||
|
"userKey": "flash",
|
||||||
|
"characterName": "»Flash«",
|
||||||
|
"class": "WI",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"weeklyPoints": 5179,
|
||||||
|
"tgCount": 7,
|
||||||
|
"currentRank": 1,
|
||||||
|
"previousRank": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userKey": "invicjusz",
|
||||||
|
"characterName": "ElementalEnchant",
|
||||||
|
"class": "FB",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"weeklyPoints": 2503,
|
||||||
|
"tgCount": 2,
|
||||||
|
"currentRank": 3,
|
||||||
|
"previousRank": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userKey": "ayana",
|
||||||
|
"characterName": "«MonkeyHunter»",
|
||||||
|
"class": "DM",
|
||||||
|
"nation": "Procyon",
|
||||||
|
"weeklyPoints": 4741,
|
||||||
|
"tgCount": 2,
|
||||||
|
"currentRank": 2,
|
||||||
|
"previousRank": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scoreIndex": {
|
||||||
|
"flash": [
|
||||||
|
"2026-06-01-20",
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"invicjusz": [
|
||||||
|
"2026-06-01-20",
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"ayana": [
|
||||||
|
"2026-06-01-20",
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"zephyr": [
|
||||||
|
"2026-06-01-20",
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"dey": [
|
||||||
|
"2026-06-01-20",
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"keira": [
|
||||||
|
"2026-06-02-20"
|
||||||
|
],
|
||||||
|
"sean": [
|
||||||
|
"2026-06-02-20"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bringer": {
|
||||||
|
"capella": null,
|
||||||
|
"procyon": null,
|
||||||
|
"procyonOverride": "»Flash«",
|
||||||
|
"capellaOverride": "XefronYokuda"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
services:
|
||||||
|
tg-bot-dev:
|
||||||
|
build:
|
||||||
|
context: /opt/docker/tg-bot-ts-dev
|
||||||
|
image: tg-bot-ts-dev:latest
|
||||||
|
container_name: tg-bot-ts-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- /opt/docker/tg-bot-ts-dev/.env
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/tg-bot-ts-dev/src:/app/src
|
||||||
|
- /opt/docker/tg-bot-ts-dev/data:/app/data
|
||||||
|
- /opt/docker/tg-bot-ts-dev/scripts:/app/scripts
|
||||||
|
- /opt/docker/tg-bot-ts-dev/messages:/app/messages
|
||||||
|
- /opt/docker/tg-bot-ts-dev/emoji-uploads:/app/emoji-uploads
|
||||||
|
- /opt/docker/tg-bot-ts-dev/tsconfig.json:/app/tsconfig.json
|
||||||
|
- /opt/docker/tg-bot-ts-dev/data/sessionPreferences.json:/app/data/sessionPreferences.json
|
||||||
BIN
emoji-uploads/bl.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
emoji-uploads/borrowed.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
emoji-uploads/capella.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
emoji-uploads/dm.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
emoji-uploads/fa.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
emoji-uploads/fb.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
emoji-uploads/fg.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
emoji-uploads/fs.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
emoji-uploads/gl.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
emoji-uploads/kd.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
emoji-uploads/luminous_bringer.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
emoji-uploads/procyon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
emoji-uploads/rank.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
emoji-uploads/score.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
emoji-uploads/storm_bringer.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
emoji-uploads/wa.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
emoji-uploads/wi.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
emoji-uploads/wrank_1.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
emoji-uploads/wrank_1_gold.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
emoji-uploads/wrank_2.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
emoji-uploads/wrank_2_gold.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
emoji-uploads/wrank_3.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
emoji-uploads/wrank_3_gold.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
emoji-uploads/wrank_4.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
emoji-uploads/wrank_4_gold.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
emoji-uploads/wrank_5.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
emoji-uploads/wrank_5_gold.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
emoji-uploads/wrank_down.png
Normal file
|
After Width: | Height: | Size: 283 B |
BIN
emoji-uploads/wrank_down_1.png
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
emoji-uploads/wrank_down_2.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
emoji-uploads/wrank_down_3.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
emoji-uploads/wrank_down_4.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
emoji-uploads/wrank_down_5.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
emoji-uploads/wrank_up.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
emoji-uploads/wrank_up_1.png
Normal file
|
After Width: | Height: | Size: 943 B |
BIN
emoji-uploads/wrank_up_2.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
emoji-uploads/wrank_up_3.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
emoji-uploads/wrank_up_4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
emoji-uploads/wrank_up_5.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -16,91 +16,26 @@
|
||||||
"storm_bringer": "<:storm_bringer:1511906496097554594>",
|
"storm_bringer": "<:storm_bringer:1511906496097554594>",
|
||||||
"wa": "<:wa:1511906499889467492>",
|
"wa": "<:wa:1511906499889467492>",
|
||||||
"wi": "<:wi:1511906503647563807>",
|
"wi": "<:wi:1511906503647563807>",
|
||||||
"wrank_1": "<:wrank_1:1512124887592996995>",
|
"wrank_1": "<:wrank_1:1511906507485085736>",
|
||||||
"wrank_1_gold": "<:wrank_1_gold:1512125051728560278>",
|
"wrank_1_gold": "<:wrank_1_gold:1511906510806978742>",
|
||||||
"wrank_2": "<:wrank_2:1512124931075342376>",
|
"wrank_2": "<:wrank_2:1511906514745430217>",
|
||||||
"wrank_2_gold": "<:wrank_2_gold:1512125095974535271>",
|
"wrank_2_gold": "<:wrank_2_gold:1511906518386212864>",
|
||||||
"wrank_3": "<:wrank_3:1512124938453254334>",
|
"wrank_3": "<:wrank_3:1511906522265944154>",
|
||||||
"wrank_3_gold": "<:wrank_3_gold:1512125103964684390>",
|
"wrank_3_gold": "<:wrank_3_gold:1511906526204530690>",
|
||||||
"wrank_4": "<:wrank_4:1512124943465316433>",
|
"wrank_4": "<:wrank_4:1511906530692173915>",
|
||||||
"wrank_4_gold": "<:wrank_4_gold:1512125108154663133>",
|
"wrank_4_gold": "<:wrank_4_gold:1511906534790266883>",
|
||||||
"wrank_5": "<:wrank_5:1512124947852431513>",
|
"wrank_5": "<:wrank_5:1511906539223388322>",
|
||||||
"wrank_5_gold": "<:wrank_5_gold:1512125112084594818>",
|
"wrank_5_gold": "<:wrank_5_gold:1511906543342452837>",
|
||||||
"wrank_down": "<:wrank_down:1511906547104616643>",
|
"wrank_down": "<:wrank_down:1511906547104616643>",
|
||||||
"wrank_down_1": "<:wrank_down_1:1512124970698801244>",
|
"wrank_down_1": "<:wrank_down_1:1511906550698999909>",
|
||||||
"wrank_down_2": "<:wrank_down_2:1512125016114729133>",
|
"wrank_down_2": "<:wrank_down_2:1511906554507694120>",
|
||||||
"wrank_down_3": "<:wrank_down_3:1512125023199166536>",
|
"wrank_down_3": "<:wrank_down_3:1511906558231969792>",
|
||||||
"wrank_down_4": "<:wrank_down_4:1512125027372237040>",
|
"wrank_down_4": "<:wrank_down_4:1511906562011304007>",
|
||||||
"wrank_down_5": "<:wrank_down_5:1512125030765691072>",
|
"wrank_down_5": "<:wrank_down_5:1511906565630984273>",
|
||||||
"wrank_neutral": "<:wrank_neutral:1511950713713070160>",
|
"wrank_up": "<:wrank_up:1511906568877117576>",
|
||||||
"wrank_neutral_0": "<:wrank_neutral_0:1511950717290545354>",
|
"wrank_up_1": "<:wrank_up_1:1511906573537120287>",
|
||||||
"wrank_no_dash": "<:wrank_no_dash:1511956379403943979>",
|
"wrank_up_2": "<:wrank_up_2:1511906577970364536>",
|
||||||
"wrank_up_1": "<:wrank_up_1:1512125132242554890>",
|
"wrank_up_3": "<:wrank_up_3:1511906581711945909>",
|
||||||
"wrank_up_10": "<:wrank_up_10:1512125136445243503>",
|
"wrank_up_4": "<:wrank_up_4:1511906585503338616>",
|
||||||
"wrank_up_2": "<:wrank_up_2:1512127569259135139>",
|
"wrank_up_5": "<:wrank_up_5:1511906588921954325>"
|
||||||
"wrank_up_3": "<:wrank_up_3:1512127577051893843>",
|
|
||||||
"wrank_up_4": "<:wrank_up_4:1512127582068281455>",
|
|
||||||
"wrank_up_6": "<:wrank_up_6:1512127590062620723>",
|
|
||||||
"wrank_up_7": "<:wrank_up_7:1512127593883766978>",
|
|
||||||
"wrank_up_8": "<:wrank_up_8:1512127598044643428>",
|
|
||||||
"wrank_up_9": "<:wrank_up_9:1512127601811128501>",
|
|
||||||
"wrank_up": "<:wrank_up:1512114414474756132>",
|
|
||||||
"wrank_up_5": "<:wrank_up_5:1512127585826377928>",
|
|
||||||
"wrank_up_11": "<:wrank_up_11:1512125140454998018>",
|
|
||||||
"wrank_up_12": "<:wrank_up_12:1512125144984719630>",
|
|
||||||
"wrank_up_13": "<:wrank_up_13:1512125149057388667>",
|
|
||||||
"wrank_up_14": "<:wrank_up_14:1512125153671123124>",
|
|
||||||
"wrank_up_15": "<:wrank_up_15:1512127541995901100>",
|
|
||||||
"wrank_up_16": "<:wrank_up_16:1512127545753993439>",
|
|
||||||
"wrank_up_17": "<:wrank_up_17:1512127549956821002>",
|
|
||||||
"wrank_up_18": "<:wrank_up_18:1512127553995931959>",
|
|
||||||
"wrank_up_19": "<:wrank_up_19:1512127558143971534>",
|
|
||||||
"wrank_up_20": "<:wrank_up_20:1512127573092597840>",
|
|
||||||
"wrank_10": "<:wrank_10:1512124891250561096>",
|
|
||||||
"wrank_11": "<:wrank_11:1512124894694080576>",
|
|
||||||
"wrank_12": "<:wrank_12:1512124898611429387>",
|
|
||||||
"wrank_13": "<:wrank_13:1512124902831030282>",
|
|
||||||
"wrank_14": "<:wrank_14:1512124907511611537>",
|
|
||||||
"wrank_15": "<:wrank_15:1512124911550730452>",
|
|
||||||
"wrank_16": "<:wrank_16:1512124915367673886>",
|
|
||||||
"wrank_17": "<:wrank_17:1512124919029305434>",
|
|
||||||
"wrank_18": "<:wrank_18:1512124923018219721>",
|
|
||||||
"wrank_19": "<:wrank_19:1512124927262855239>",
|
|
||||||
"wrank_20": "<:wrank_20:1512124934762135684>",
|
|
||||||
"wrank_6": "<:wrank_6:1512124952738795581>",
|
|
||||||
"wrank_7": "<:wrank_7:1512124956622979143>",
|
|
||||||
"wrank_8": "<:wrank_8:1512124961450496020>",
|
|
||||||
"wrank_9": "<:wrank_9:1512124965363650631>",
|
|
||||||
"wrank_down_10": "<:wrank_down_10:1512124974582989000>",
|
|
||||||
"wrank_down_11": "<:wrank_down_11:1512124978504536114>",
|
|
||||||
"wrank_down_12": "<:wrank_down_12:1512124982728069192>",
|
|
||||||
"wrank_down_13": "<:wrank_down_13:1512124987501314150>",
|
|
||||||
"wrank_down_14": "<:wrank_down_14:1512124991292837918>",
|
|
||||||
"wrank_down_15": "<:wrank_down_15:1512124995340468335>",
|
|
||||||
"wrank_down_16": "<:wrank_down_16:1512124999576850462>",
|
|
||||||
"wrank_down_17": "<:wrank_down_17:1512125004353896642>",
|
|
||||||
"wrank_down_18": "<:wrank_down_18:1512125008132964486>",
|
|
||||||
"wrank_down_19": "<:wrank_down_19:1512125011857510410>",
|
|
||||||
"wrank_down_20": "<:wrank_down_20:1512125019814101173>",
|
|
||||||
"wrank_down_6": "<:wrank_down_6:1512125035123576922>",
|
|
||||||
"wrank_down_7": "<:wrank_down_7:1512125039091126434>",
|
|
||||||
"wrank_down_8": "<:wrank_down_8:1512125042757210123>",
|
|
||||||
"wrank_down_9": "<:wrank_down_9:1512125047798759706>",
|
|
||||||
"wrank_10_gold": "<:wrank_10_gold:1512125055432396910>",
|
|
||||||
"wrank_11_gold": "<:wrank_11_gold:1512125059123249242>",
|
|
||||||
"wrank_12_gold": "<:wrank_12_gold:1512125063304974438>",
|
|
||||||
"wrank_13_gold": "<:wrank_13_gold:1512125067201609908>",
|
|
||||||
"wrank_14_gold": "<:wrank_14_gold:1512125071043596520>",
|
|
||||||
"wrank_15_gold": "<:wrank_15_gold:1512125074893832443>",
|
|
||||||
"wrank_16_gold": "<:wrank_16_gold:1512125078966374420>",
|
|
||||||
"wrank_17_gold": "<:wrank_17_gold:1512125083605532715>",
|
|
||||||
"wrank_18_gold": "<:wrank_18_gold:1512125088378392718>",
|
|
||||||
"wrank_19_gold": "<:wrank_19_gold:1512125091960459508>",
|
|
||||||
"wrank_20_gold": "<:wrank_20_gold:1512125100265181204>",
|
|
||||||
"wrank_6_gold": "<:wrank_6_gold:1512125115956203601>",
|
|
||||||
"wrank_7_gold": "<:wrank_7_gold:1512125120204771338>",
|
|
||||||
"wrank_8_gold": "<:wrank_8_gold:1512125123874918661>",
|
|
||||||
"wrank_9_gold": "<:wrank_9_gold:1512125128299905104>",
|
|
||||||
"wrank_no_rank": "<:wrank_no_rank:1512261782205628606>",
|
|
||||||
"wrank_no_rank_delta": "<:wrank_no_rank_delta:1512263603519229982>"
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* Bulk emoji upload script with subdirectory support and round-robin distribution.
|
* Bulk emoji upload script
|
||||||
|
* Usage: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts [emoji_dir]
|
||||||
*
|
*
|
||||||
* Usage:
|
* Distributes emojis across a pool of donor servers (round-robin by available capacity).
|
||||||
* Upload: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts [emoji_dir]
|
* Each emoji is unique across all servers — no duplicates.
|
||||||
* Delete: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts --delete <pattern>
|
* Automatically updates messages/emojis.json with the uploaded emoji IDs.
|
||||||
* Pattern can be a prefix (e.g. "wrank_up") or exact name (e.g. "wrank_up_1")
|
|
||||||
* Multiple patterns: --delete wrank_up wrank_down wrank_gold
|
|
||||||
*
|
|
||||||
* Directory naming conventions:
|
|
||||||
* - Files in root dir → name = filename without extension
|
|
||||||
* - Files in subdir → name determined by DIR_NAME_MAP or default (dirname_filename)
|
|
||||||
* - Passthrough dirs → name = filename only (no prefix)
|
|
||||||
*
|
*
|
||||||
* Required .env vars:
|
* Required .env vars:
|
||||||
* DISCORD_TOKEN — bot token
|
* DISCORD_TOKEN — bot token
|
||||||
* EMOJI_DONOR_GUILDS — comma-separated donor server IDs
|
* EMOJI_DONOR_GUILDS — comma-separated donor server IDs
|
||||||
|
* e.g. EMOJI_DONOR_GUILDS=111111111111,222222222222,333333333333
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { REST, Routes } from "discord.js";
|
import { REST, Routes } from "discord.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
// Load .env
|
// Load .env manually since we're outside the bot runtime
|
||||||
const envPath = path.join(__dirname, "../.env");
|
const envPath = path.join(__dirname, "../.env");
|
||||||
if (fs.existsSync(envPath)) {
|
if (fs.existsSync(envPath)) {
|
||||||
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
|
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
|
||||||
|
|
@ -32,239 +27,185 @@
|
||||||
|
|
||||||
const TOKEN = process.env.DISCORD_TOKEN!;
|
const TOKEN = process.env.DISCORD_TOKEN!;
|
||||||
const DONOR_GUILD_IDS: string[] = (process.env.EMOJI_DONOR_GUILDS ?? "")
|
const DONOR_GUILD_IDS: string[] = (process.env.EMOJI_DONOR_GUILDS ?? "")
|
||||||
.split(",").map((id) => id.trim()).filter(Boolean);
|
.split(",")
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (!TOKEN || DONOR_GUILD_IDS.length === 0) {
|
if (!TOKEN) {
|
||||||
console.error("❌ DISCORD_TOKEN and EMOJI_DONOR_GUILDS must be set in .env");
|
console.error("❌ DISCORD_TOKEN must be set in .env");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (DONOR_GUILD_IDS.length === 0) {
|
||||||
|
console.error("❌ EMOJI_DONOR_GUILDS must be set in .env (comma-separated guild IDs)");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojiDir = path.join(__dirname, "../emoji-uploads");
|
const emojiDir = process.argv[2] ?? path.join(__dirname, "../emoji-uploads");
|
||||||
const emojisPath = path.join(__dirname, "../messages/emojis.json");
|
const emojisPath = path.join(__dirname, "../messages/emojis.json");
|
||||||
const rest = new REST({ version: "10" }).setToken(TOKEN);
|
|
||||||
|
|
||||||
// ─── Naming config ─────────────────────────────────────────────────────────────
|
if (!fs.existsSync(emojiDir)) {
|
||||||
|
console.error(`❌ Emoji directory not found: ${emojiDir}`);
|
||||||
// Dirs listed here use filename only — no dir prefix
|
console.error(` Create it and place your emoji PNG files inside.`);
|
||||||
const PASSTHROUGH_DIRS: string[] = ["classes", "nations", "misc"];
|
process.exit(1);
|
||||||
|
|
||||||
// Custom naming functions per dir — (filename without ext) → emoji name
|
|
||||||
const DIR_NAME_MAP: Record<string, (filename: string) => string> = {
|
|
||||||
"wrank": (f) => `wrank_${f}`,
|
|
||||||
"wrank_gold": (f) => `wrank_${f}_gold`,
|
|
||||||
"wrank_up": (f) => `wrank_up_${f}`,
|
|
||||||
"wrank_down": (f) => `wrank_down_${f}`,
|
|
||||||
"wrank_x": (f) => `wrank_x_${f}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveEmojiName(dirName: string, filename: string): string {
|
|
||||||
if (PASSTHROUGH_DIRS.includes(dirName)) return filename;
|
|
||||||
if (DIR_NAME_MAP[dirName]) return DIR_NAME_MAP[dirName](filename);
|
|
||||||
return `${dirName}_${filename}`; // default: dirname_filename
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── File discovery ────────────────────────────────────────────────────────────
|
const rest = new REST({ version: "10" }).setToken(TOKEN);
|
||||||
|
|
||||||
interface EmojiFile {
|
interface GuildEmojiSlot {
|
||||||
emojiName: string;
|
guildId: string;
|
||||||
filePath: string;
|
name: string; // guild name for display
|
||||||
mimeType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IMAGE_EXTS = [".png", ".jpg", ".gif", ".webp"];
|
|
||||||
|
|
||||||
function mimeFor(ext: string): string {
|
|
||||||
if (ext === ".gif") return "image/gif";
|
|
||||||
if (ext === ".webp") return "image/webp";
|
|
||||||
return "image/png";
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanDir(dir: string, parentDirName?: string): EmojiFile[] {
|
|
||||||
const results: EmojiFile[] = [];
|
|
||||||
if (!fs.existsSync(dir)) return results;
|
|
||||||
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
results.push(...scanDir(fullPath, entry.name));
|
|
||||||
} else {
|
|
||||||
const ext = path.extname(entry.name).toLowerCase();
|
|
||||||
if (!IMAGE_EXTS.includes(ext)) continue;
|
|
||||||
const filename = path.basename(entry.name, ext);
|
|
||||||
const emojiName = parentDirName
|
|
||||||
? resolveEmojiName(parentDirName, filename)
|
|
||||||
: filename;
|
|
||||||
results.push({ emojiName, filePath: fullPath, mimeType: mimeFor(ext) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Guild helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
interface GuildSlot {
|
|
||||||
guildId: string;
|
|
||||||
name: string;
|
|
||||||
existing: Map<string, string>; // emojiName → emojiId
|
existing: Map<string, string>; // emojiName → emojiId
|
||||||
capacity: number;
|
capacity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxEmojisForTier(tier: number): number {
|
// Compute max emojis based on Nitro boost tier
|
||||||
return [50, 100, 150, 250][tier] ?? 50;
|
function maxEmojisForTier(premiumTier: number): number {
|
||||||
|
switch (premiumTier) {
|
||||||
|
case 1: return 100;
|
||||||
|
case 2: return 150;
|
||||||
|
case 3: return 250;
|
||||||
|
default: return 50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGuildSlots(): Promise<GuildSlot[]> {
|
async function fetchGuildSlots(guildIds: string[]): Promise<GuildEmojiSlot[]> {
|
||||||
const slots: GuildSlot[] = [];
|
const slots: GuildEmojiSlot[] = [];
|
||||||
for (const guildId of DONOR_GUILD_IDS) {
|
|
||||||
|
for (const guildId of guildIds) {
|
||||||
try {
|
try {
|
||||||
const [guild, emojis] = await Promise.all([
|
const [guild, emojis] = await Promise.all([
|
||||||
rest.get(Routes.guild(guildId)) as Promise<any>,
|
rest.get(Routes.guild(guildId)) as Promise<any>,
|
||||||
rest.get(Routes.guildEmojis(guildId)) as Promise<any[]>,
|
rest.get(Routes.guildEmojis(guildId)) as Promise<any[]>,
|
||||||
]);
|
]);
|
||||||
const max = maxEmojisForTier(guild.premium_tier ?? 0);
|
|
||||||
const existing = new Map(emojis.map((e: any) => [e.name, e.id]));
|
const maxEmojis = maxEmojisForTier(guild.premium_tier ?? 0);
|
||||||
const capacity = max - emojis.length;
|
const existingMap = new Map(emojis.map((e: any) => [e.name, e.id]));
|
||||||
console.log(`🏠 ${guild.name} (${guildId}): ${emojis.length}/${max} emojis, ${capacity} free`);
|
const capacity = maxEmojis - emojis.length;
|
||||||
slots.push({ guildId, name: guild.name, existing, capacity });
|
const guildName = guild.name ?? guildId;
|
||||||
|
|
||||||
|
console.log(`🏠 ${guildName} (${guildId}): ${emojis.length}/${maxEmojis} emojis, ${capacity} slots free`);
|
||||||
|
slots.push({ guildId, name: guildName, existing: existingMap, capacity });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`❌ Could not fetch guild ${guildId}: ${err.message}`);
|
console.error(`❌ Could not fetch guild ${guildId}: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Upload ────────────────────────────────────────────────────────────────────
|
async function uploadEmojis(): Promise<void> {
|
||||||
|
const files = fs.readdirSync(emojiDir).filter((f) =>
|
||||||
|
[".png", ".jpg", ".gif", ".webp"].includes(path.extname(f).toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
async function upload(): Promise<void> {
|
|
||||||
const files = scanDir(emojiDir);
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
console.error(`❌ No image files found in ${emojiDir}`);
|
console.error("❌ No image files found in the emoji directory.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load existing emojis.json
|
||||||
let emojiMap: Record<string, string> = {};
|
let emojiMap: Record<string, string> = {};
|
||||||
try { emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); } catch {}
|
try {
|
||||||
|
emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
console.warn("⚠️ Could not load emojis.json — will create fresh mapping.");
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n📁 Found ${files.length} file(s)\n🔍 Scanning donor servers...\n`);
|
console.log(`\n📁 Found ${files.length} file(s) in ${emojiDir}`);
|
||||||
const slots = await fetchGuildSlots();
|
console.log(`🔍 Scanning ${DONOR_GUILD_IDS.length} donor server(s)...\n`);
|
||||||
if (slots.length === 0) { console.error("❌ No accessible donor servers."); process.exit(1); }
|
|
||||||
|
|
||||||
// Build global dedup map
|
const guildSlots = await fetchGuildSlots(DONOR_GUILD_IDS);
|
||||||
const globalExisting = new Map<string, string>();
|
|
||||||
for (const slot of slots) {
|
if (guildSlots.length === 0) {
|
||||||
|
console.error("❌ No accessible donor guilds found. Check EMOJI_DONOR_GUILDS and bot membership.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build global deduplication map across all donor guilds
|
||||||
|
const globalExisting = new Map<string, string>(); // emojiName → formatted string
|
||||||
|
for (const slot of guildSlots) {
|
||||||
for (const [name, id] of slot.existing) {
|
for (const [name, id] of slot.existing) {
|
||||||
globalExisting.set(name, `<:${name}:${id}>`);
|
globalExisting.set(name, `<:${name}:${id}>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCapacity = slots.reduce((s, g) => s + g.capacity, 0);
|
const totalCapacity = guildSlots.reduce((sum, s) => sum + s.capacity, 0);
|
||||||
console.log(`\n📊 ${globalExisting.size} existing · ${totalCapacity} slots free\n`);
|
console.log(`\n📊 ${globalExisting.size} emoji(s) already exist · ${totalCapacity} slots available across all servers\n`);
|
||||||
|
|
||||||
let slotIndex = 0;
|
if (totalCapacity === 0) {
|
||||||
function nextSlot(): GuildSlot | null {
|
console.error("❌ All donor servers are full! Add more servers to EMOJI_DONOR_GUILDS.");
|
||||||
const start = slotIndex;
|
process.exit(1);
|
||||||
do {
|
|
||||||
const s = slots[slotIndex % slots.length];
|
|
||||||
slotIndex++;
|
|
||||||
if (s.capacity > 0) return s;
|
|
||||||
} while (slotIndex % slots.length !== start % slots.length);
|
|
||||||
return slots.find((s) => s.capacity > 0) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploaded = 0, skipped = 0, failed = 0;
|
let uploaded = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
// Round-robin slot picker — distributes load evenly across guilds
|
||||||
|
let slotIndex = 0;
|
||||||
|
function nextAvailableSlot(): GuildEmojiSlot | null {
|
||||||
|
const start = slotIndex;
|
||||||
|
do {
|
||||||
|
const slot = guildSlots[slotIndex % guildSlots.length];
|
||||||
|
slotIndex++;
|
||||||
|
if (slot.capacity > 0) return slot;
|
||||||
|
} while (slotIndex % guildSlots.length !== start % guildSlots.length);
|
||||||
|
// Fallback: find any with capacity (in case loop exited without finding one)
|
||||||
|
return guildSlots.find((s) => s.capacity > 0) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (globalExisting.has(file.emojiName)) {
|
const emojiName = path.basename(file, path.extname(file));
|
||||||
emojiMap[file.emojiName] = globalExisting.get(file.emojiName)!;
|
const filePath = path.join(emojiDir, file);
|
||||||
console.log(`⏭️ Exists: ${file.emojiName} → ${emojiMap[file.emojiName]}`);
|
const ext = path.extname(file).toLowerCase();
|
||||||
|
const mimeType = ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/png";
|
||||||
|
|
||||||
|
// Already exists in the pool — update map and skip
|
||||||
|
if (globalExisting.has(emojiName)) {
|
||||||
|
emojiMap[emojiName] = globalExisting.get(emojiName)!;
|
||||||
|
console.log(`⏭️ Already exists: ${emojiName} → ${emojiMap[emojiName]}`);
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const slot = nextSlot();
|
const slot = nextAvailableSlot();
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
console.error(`❌ No slots available for: ${file.emojiName}`);
|
console.error(`❌ All slots full — could not upload: ${emojiName}`);
|
||||||
|
console.error(` Add more servers to EMOJI_DONOR_GUILDS in .env`);
|
||||||
failed++;
|
failed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const base64 = `data:${file.mimeType};base64,${fs.readFileSync(file.filePath).toString("base64")}`;
|
const base64 = `data:${mimeType};base64,${fs.readFileSync(filePath).toString("base64")}`;
|
||||||
const result = await rest.post(Routes.guildEmojis(slot.guildId), {
|
const result = await rest.post(Routes.guildEmojis(slot.guildId), {
|
||||||
body: { name: file.emojiName, image: base64 },
|
body: { name: emojiName, image: base64 },
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
const formatted = `<:${file.emojiName}:${result.id}>`;
|
const formatted = `<:${emojiName}:${result.id}>`;
|
||||||
emojiMap[file.emojiName] = formatted;
|
emojiMap[emojiName] = formatted;
|
||||||
slot.capacity--;
|
slot.capacity--;
|
||||||
console.log(`✅ Uploaded: ${file.emojiName} → ${formatted} [${slot.name}]`);
|
|
||||||
|
console.log(`✅ Uploaded: ${emojiName} → ${formatted} [${slot.name}]`);
|
||||||
uploaded++;
|
uploaded++;
|
||||||
|
|
||||||
|
// Rate limit buffer
|
||||||
await new Promise((r) => setTimeout(r, 600));
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`❌ Failed: ${file.emojiName} — ${err.message}`);
|
console.error(`❌ Failed: ${emojiName} — ${err.message}`);
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2));
|
fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2));
|
||||||
|
|
||||||
console.log(`\n📊 ${uploaded} uploaded · ${skipped} skipped · ${failed} failed`);
|
console.log(`\n📊 ${uploaded} uploaded · ${skipped} skipped · ${failed} failed`);
|
||||||
console.log(`💾 messages/emojis.json updated`);
|
console.log(`💾 messages/emojis.json updated`);
|
||||||
|
console.log(`\nSlot usage after upload:`);
|
||||||
|
for (const slot of guildSlots) {
|
||||||
|
const used = slot.existing.size + (uploaded > 0 ? slot.existing.size : 0);
|
||||||
|
console.log(` ${slot.name}: ${slot.capacity} slots remaining`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Delete ────────────────────────────────────────────────────────────────────
|
uploadEmojis().catch(console.error);
|
||||||
|
|
||||||
async function deleteEmojis(patterns: string[]): Promise<void> {
|
|
||||||
console.log(`\n🗑️ Deleting emojis matching: ${patterns.join(", ")}`);
|
|
||||||
console.log(`🔍 Scanning donor servers...\n`);
|
|
||||||
|
|
||||||
const slots = await fetchGuildSlots();
|
|
||||||
if (slots.length === 0) { console.error("❌ No accessible donor servers."); process.exit(1); }
|
|
||||||
|
|
||||||
let emojiMap: Record<string, string> = {};
|
|
||||||
try { emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); } catch {}
|
|
||||||
|
|
||||||
let deleted = 0, failed = 0;
|
|
||||||
|
|
||||||
for (const slot of slots) {
|
|
||||||
for (const [name, id] of slot.existing) {
|
|
||||||
const matches = patterns.some((p) => name === p || name.startsWith(`${p}_`) || name.startsWith(p));
|
|
||||||
if (!matches) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await rest.delete(Routes.guildEmoji(slot.guildId, id));
|
|
||||||
console.log(`🗑️ Deleted: ${name} [${slot.name}]`);
|
|
||||||
slot.existing.delete(name);
|
|
||||||
delete emojiMap[name];
|
|
||||||
deleted++;
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(`❌ Failed to delete ${name}: ${err.message}`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2));
|
|
||||||
console.log(`\n📊 ${deleted} deleted · ${failed} failed`);
|
|
||||||
console.log(`💾 messages/emojis.json updated`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Entry point ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
|
|
||||||
if (args[0] === "--delete") {
|
|
||||||
const patterns = args.slice(1);
|
|
||||||
if (patterns.length === 0) {
|
|
||||||
console.error("❌ Specify at least one pattern: --delete <pattern> [pattern2] ...");
|
|
||||||
console.error(" Examples:");
|
|
||||||
console.error(" --delete wrank_up (deletes wrank_up_1, wrank_up_2, ...)");
|
|
||||||
console.error(" --delete wrank_up_1 (deletes exact match)");
|
|
||||||
console.error(" --delete wrank_up wrank_down wrank_gold");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
deleteEmojis(patterns).catch(console.error);
|
|
||||||
} else {
|
|
||||||
upload().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
@ -49,6 +49,15 @@ export async function handleRankPost(interaction: ChatInputCommandInteraction):
|
||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
|
// const embed = new EmbedBuilder()
|
||||||
|
// .setTitle(`⚔️ W.Rank Leaderboard — ${weekKey}`)
|
||||||
|
// .setColor(0xe8a317)
|
||||||
|
// .addFields(
|
||||||
|
// { name: "🔵 Capella", value: formatNation("capella"), inline: true },
|
||||||
|
// { name: "🔴 Procyon", value: formatNation("procyon"), inline: true },
|
||||||
|
// )
|
||||||
|
// .setTimestamp();
|
||||||
|
|
||||||
const channelId = cfg("resultsChannelId") || cfg("pollChannelId");
|
const channelId = cfg("resultsChannelId") || cfg("pollChannelId");
|
||||||
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
||||||
await channel.send({ embeds: [embed] });
|
await channel.send({ embeds: [embed] });
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ function wrankDelta(entry: WRankEntry, options?: { brackets?: boolean }): string
|
||||||
const numEmoji = getEmoji(`wrank_down_${delta}`);
|
const numEmoji = getEmoji(`wrank_down_${delta}`);
|
||||||
inner = (getEmoji("wrank_down") || "↓") + (numEmoji || delta);
|
inner = (getEmoji("wrank_down") || "↓") + (numEmoji || delta);
|
||||||
} else {
|
} else {
|
||||||
inner = (getEmoji("wrank_no_dash") || "·") + (getEmoji("wrank_neutral_0") || "0");
|
inner = (getEmoji("wrank_neutral") || "·") + "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
return brackets ? ` (${inner})` : ` ${inner}`;
|
return brackets ? ` (${inner})` : ` ${inner}`;
|
||||||
|
|
@ -106,18 +106,6 @@ function wrankFull(entry: WRankEntry, options: WRankDisplayOptions): string {
|
||||||
return wrankRank(entry, options.goal) + wrankDelta(entry, { brackets: options.brackets });
|
return wrankRank(entry, options.goal) + wrankDelta(entry, { brackets: options.brackets });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder for characters with no W.Rank when others in their nation have one.
|
|
||||||
* Output: — ( [] — )
|
|
||||||
*/
|
|
||||||
function wrankNoRank(): string {
|
|
||||||
const norank = getEmoji("wrank_no_dash") || "—";
|
|
||||||
const dash = getEmoji("wrank_no_rank_delta") || "—";
|
|
||||||
const square = getEmoji("wrank_no_dash") || "■";
|
|
||||||
return `${norank} (${square}${dash})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ─── Namespace export ─────────────────────────────────────────────────────────
|
// ─── Namespace export ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const format = {
|
export const format = {
|
||||||
|
|
@ -129,6 +117,5 @@ export const format = {
|
||||||
rank: wrankRank,
|
rank: wrankRank,
|
||||||
delta: wrankDelta,
|
delta: wrankDelta,
|
||||||
full: wrankFull,
|
full: wrankFull,
|
||||||
noRank: wrankNoRank,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -69,45 +69,18 @@ export function lockPoll(slot: number): void {
|
||||||
|
|
||||||
persist.save(polls)
|
persist.save(polls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Character display ────────────────────────────────────────────────────────
|
// ─── Character display ────────────────────────────────────────────────────────
|
||||||
function getNationBringerTitle(nation: Nation) {
|
function formatCharRow(entry: VoteEntry, showNationEmoji = false): string {
|
||||||
const stormBringerIcon = getEmoji("storm_bringer") || "⚡";
|
|
||||||
const stormBringer = `${stormBringerIcon}`;
|
|
||||||
|
|
||||||
const luminousBringerIcon = getEmoji("luminous_bringer") || "🔆";
|
|
||||||
const luminousBringer = `${luminousBringerIcon}` || `🔆 Luminous Bringer`;
|
|
||||||
|
|
||||||
const nationMap = {
|
|
||||||
"Capella": luminousBringer,
|
|
||||||
"Procyon": stormBringer
|
|
||||||
};
|
|
||||||
|
|
||||||
return nationMap[nation];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBringerDisplay(nation: Nation): string {
|
|
||||||
const bringerMap: Record<Nation, string> = {
|
|
||||||
Capella: getEmoji("luminous_bringer") || "🔆 Luminous Bringer",
|
|
||||||
Procyon: getEmoji("storm_bringer") || "⚡ Storm Bringer",
|
|
||||||
};
|
|
||||||
return bringerMap[nation];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCharRow(entry: VoteEntry, showNationEmoji = false, nationHasRank = false): string {
|
|
||||||
const cfgFormat = cfg("charDisplayFormat");
|
const cfgFormat = cfg("charDisplayFormat");
|
||||||
const nation = entry.characterNation;
|
const nation = entry.characterNation;
|
||||||
const wRankEntry = entry.characterName && entry.characterNation
|
const wRankEntry = entry.characterName ? getEntry(entry.characterName, nation ?? "Capella") : null;
|
||||||
? getEntry(entry.characterName, entry.characterNation)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let wrank = "";
|
let wrank = "";
|
||||||
if (wRankEntry) {
|
if (wRankEntry) {
|
||||||
const wRankGoal = cfg("wRankGoal");
|
const wRankGoal = cfg("wRankGoal");
|
||||||
wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true });
|
wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true });
|
||||||
} else if (nationHasRank) {
|
|
||||||
wrank = format.wrank.noRank();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const classStr = entry.characterClass
|
const classStr = entry.characterClass
|
||||||
|
|
@ -127,11 +100,33 @@ function formatCharRow(entry: VoteEntry, showNationEmoji = false, nationHasRank
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Bringer title — independent of W.Rank so override always shows
|
// Bringer title — independent of W.Rank so override always shows
|
||||||
|
function getNationBringerTitle(nation: Nation) {
|
||||||
|
const stormBringerIcon = getEmoji("storm_bringer") || "⚡";
|
||||||
|
const stormBringer = `${stormBringerIcon}`;
|
||||||
|
|
||||||
|
const luminousBringerIcon = getEmoji("luminous_bringer") || "⚡";
|
||||||
|
const luminousBringer = `${luminousBringerIcon}` || `⚡ Luminous Bringer`;
|
||||||
|
|
||||||
|
const nationMap = {
|
||||||
|
"Capella": luminousBringer,
|
||||||
|
"Procyon": stormBringer
|
||||||
|
};
|
||||||
|
|
||||||
|
return nationMap[nation];
|
||||||
|
}
|
||||||
if (nation && entry.userKey) {
|
if (nation && entry.userKey) {
|
||||||
const bringer = getBringer(nation);
|
const bringer = getBringer(nation);
|
||||||
if (bringer && bringer === entry.characterName) {
|
if (bringer && bringer === entry.characterName) {
|
||||||
row += ` · ${getBringerDisplay(nation)}`;
|
const bringerTitle = getNationBringerTitle(nation);
|
||||||
|
row += ` · ${bringerTitle}`;
|
||||||
}
|
}
|
||||||
|
// if (bringer && bringer === entry.characterName) {
|
||||||
|
// const emoji = nation === "Capella"
|
||||||
|
// ? (getEmoji("luminous_bringer") || "🔆")
|
||||||
|
// : (getEmoji("storm_bringer") || "⚡");
|
||||||
|
// const title = nation === "Capella" ? "Luminous Bringer" : "Storm Bringer";
|
||||||
|
// row += ` · ${emoji} **${title}**`;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.borrowedFrom) {
|
if (entry.borrowedFrom) {
|
||||||
|
|
@ -165,13 +160,12 @@ export function buildEmbed(state: PollState, overrideLockMsg?: string): EmbedBui
|
||||||
|
|
||||||
const formatNationField = (nation: Nation): string => {
|
const formatNationField = (nation: Nation): string => {
|
||||||
const yesEntries = yesByNation[nation];
|
const yesEntries = yesByNation[nation];
|
||||||
const hasRank = yesEntries.some((e) => e.characterName && getEntry(e.characterName, nation) !== null);
|
|
||||||
const noEntries = showNoInline
|
const noEntries = showNoInline
|
||||||
? noVoters.filter((e) => e.characterNation === nation)
|
? noVoters.filter((e) => e.characterNation === nation)
|
||||||
: [];
|
: [];
|
||||||
const lines = [
|
const lines = [
|
||||||
...yesEntries.map((e) => formatCharRow(e, false, hasRank)),
|
...yesEntries.map((e) => formatCharRow(e)),
|
||||||
...noEntries.map((e) => `❌ ${formatCharRow(e, false, hasRank)}`),
|
...noEntries.map((e) => `❌ ${formatCharRow(e)}`),
|
||||||
];
|
];
|
||||||
return lines.length > 0 ? lines.join("\n") : "—";
|
return lines.length > 0 ? lines.join("\n") : "—";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,16 +99,12 @@ export function recordScore(
|
||||||
}
|
}
|
||||||
|
|
||||||
function recomputeRanks(week: WRankWeek, nation: Nation): void {
|
function recomputeRanks(week: WRankWeek, nation: Nation): void {
|
||||||
const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
|
const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
|
||||||
const sorted = [...list].sort((a, b) => b.weeklyPoints - a.weeklyPoints);
|
const sorted = [...list].sort((a, b) => b.weeklyPoints - a.weeklyPoints);
|
||||||
sorted.forEach((entry, i) => {
|
sorted.forEach((entry, i) => {
|
||||||
const live = list.find((e) => e.characterName === entry.characterName)!;
|
const live = list.find((e) => e.characterName === entry.characterName)!;
|
||||||
const newRank = i + 1;
|
live.previousRank = live.currentRank || undefined;
|
||||||
// Only snapshot previousRank when rank actually changes
|
live.currentRank = i + 1;
|
||||||
if (live.currentRank !== 0 && live.currentRank !== newRank) {
|
|
||||||
live.previousRank = live.currentRank;
|
|
||||||
}
|
|
||||||
live.currentRank = newRank;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,8 +146,6 @@ export function getBringer(nation: Nation): string | null {
|
||||||
export function getEntry(characterName: string, nation: Nation): WRankEntry | null {
|
export function getEntry(characterName: string, nation: Nation): WRankEntry | null {
|
||||||
const week = getCurrentWeek();
|
const week = getCurrentWeek();
|
||||||
const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
|
const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
|
||||||
console.log(`[getEntry] weekKey=${week.weekKey} nation=${nation} listLength=${list?.length} looking for=${characterName}`);
|
|
||||||
console.log(`[getEntry] available:`, list?.map(e => e.characterName));
|
|
||||||
return list.find((e) => e.characterName === characterName) ?? null;
|
return list.find((e) => e.characterName === characterName) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"@src/*": ["src/*"],
|
"@src/*": ["src/*"],
|
||||||
"@data/*": ["data/*"],
|
"@data/*": ["data/*"],
|
||||||
"@tests/*": ["tests/*"],
|
|
||||||
"@messages/*": ["messages/*"],
|
"@messages/*": ["messages/*"],
|
||||||
"@tgHistory/*": ["data/tg-history/*"],
|
"@tgHistory/*": ["data/tg-history/*"],
|
||||||
"@scripts/*": ["scripts/*"],
|
"@scripts/*": ["scripts/*"],
|
||||||
|
|
|
||||||