增加游戏内任务相关接口

This commit is contained in:
CounterFire2023 2024-05-11 10:35:33 +08:00
parent e5d2e653e8
commit 2a145b7f37
36 changed files with 1950 additions and 31 deletions

View File

@ -2,11 +2,11 @@
{ {
"type": 1, "type": 1,
"name": "White List", "name": "White List",
"probability": 100 "probability": 2
}, },
{ {
"type": 2, "type": 2,
"name": "NFT", "name": "NFT",
"probability": 100 "probability": 1
} }
] ]

65
configs/draw_cfg.json Normal file
View File

@ -0,0 +1,65 @@
[
{
"position": 1,
"type": 1,
"name": "WhiteList",
"amount": 1,
"probability": 100
},
{
"position": 2,
"type": 0,
"amount": 60,
"probability": 1482
},
{
"position": 3,
"type": 0,
"amount": 30,
"probability": 2004
},
{
"position": 4,
"type": 0,
"amount": 240,
"probability": 157
},
{
"position": 5,
"type": 0,
"amount": 30,
"probability": 2004
},
{
"position": 6,
"type": 2,
"name": "NFT",
"amount": 1,
"probability": 100
},
{
"position": 7,
"type": 0,
"amount": 120,
"probability": 434
},
{
"position": 8,
"type": 0,
"amount": 30,
"probability": 2004
},
{
"position": 9,
"type": 0,
"amount": 120,
"probability": 434
},
{
"position": 10,
"type": 0,
"amount": 50,
"probability": 1482
}
]

111
configs/ingame_tasks.json Normal file
View File

@ -0,0 +1,111 @@
[
{
"id": "g3885fwjnddej6jmcha",
"title": "Bind Game Account",
"task": "GoogleConnect",
"show": false,
"type": 1,
"category": 1,
"desc": "",
"ticket": 0,
"score": 0,
"checkChain": false,
"autoclaim": true
},
{
"id": "g3zkyh1rk6qs5exdefe",
"title": "登录游戏",
"desc": "",
"task": "GameLogin",
"show": true,
"type": 2,
"category": 1,
"pretasks": ["g3885fwjnddej6jmcha"],
"ticket": 0,
"score": 10,
"checkChain": false,
"cfg": { "key": "loginVal", "amount": 1 },
"autoclaim": true
},
{
"id": "g3amegdkchm4pdjtjeb",
"title": "参与3场任意模式的比赛",
"desc": "",
"task": "GameBattle",
"show": true,
"type": 2,
"category": 1,
"pretasks": ["g3885fwjnddej6jmcha"],
"ticket": 0,
"tickettasks": ["g31x9wzja7eg18t3vtm"],
"score": 20,
"checkChain": false,
"cfg": { "key": "battleTimes", "amount": 3 },
"autoclaim": true
},
{
"id": "g31x9wzja7eg18t3vtm",
"title": "累计完成10次击杀",
"desc": "",
"task": "GameKill",
"show": true,
"type": 2,
"category": 1,
"pretasks": ["g3885fwjnddej6jmcha"],
"ticket": 1,
"tickettasks": ["g3amegdkchm4pdjtjeb"],
"score": 20,
"checkChain": false,
"cfg": { "key": "kills", "amount": 10 },
"autoclaim": true
},
{
"id": "g38gejrp3twh1gvtr2e",
"title": "在Bounty或Rank模式下获得3次胜利",
"desc": "",
"task": "GameWin",
"show": true,
"type": 2,
"category": 2,
"pretasks": ["g3885fwjnddej6jmcha"],
"ticket": 0,
"score": 25,
"checkChain": false,
"checkTime": true,
"cfg": { "key": "winTimes", "amount": 3 },
"autoclaim": true
},
{
"id": "g33xf6688x67a2mdrrk",
"title": "在Gold模式下获取500金币",
"desc": "",
"task": "GameCoin",
"show": true,
"category": 2,
"pretasks": ["g3885fwjnddej6jmcha"],
"type": 2,
"ticket": 0,
"score": 25,
"checkTime": true,
"checkChain": false,
"cfg": { "key": "getGoldVal","amount": 500 },
"autoclaim": true
},
{
"id": "g3s6esb83da9wy9p25a",
"title": "Own CounterFile|Founder's Tag",
"desc": "",
"task": "NftHolderCheck",
"show": true,
"pretasks": ["g3885fwjnddej6jmcha"],
"type": 2,
"category": 3,
"ticket": 0,
"score": 100,
"checkChain": false,
"checkTime": true,
"cfg": { "address": "0xec23679653337d4c6390d0eeba682246a6067777", "chain": "1" },
"autoclaim": true
}
]

503
configs/nft_202403.json Normal file
View File

@ -0,0 +1,503 @@
[
{"rarity":"Common","id":1},
{"rarity":"Rare","id":2},
{"rarity":"Common","id":3},
{"rarity":"Common","id":4},
{"rarity":"Common","id":5},
{"rarity":"Common","id":6},
{"rarity":"Common","id":7},
{"rarity":"Common","id":8},
{"rarity":"Common","id":9},
{"rarity":"Common","id":10},
{"rarity":"Common","id":11},
{"rarity":"Common","id":12},
{"rarity":"Common","id":13},
{"rarity":"Common","id":14},
{"rarity":"Common","id":15},
{"rarity":"Common","id":16},
{"rarity":"Common","id":17},
{"rarity":"Common","id":18},
{"rarity":"Common","id":19},
{"rarity":"Common","id":20},
{"rarity":"Common","id":21},
{"rarity":"Common","id":22},
{"rarity":"Common","id":23},
{"rarity":"Common","id":24},
{"rarity":"Common","id":25},
{"rarity":"Common","id":26},
{"rarity":"Common","id":27},
{"rarity":"Common","id":28},
{"rarity":"Common","id":29},
{"rarity":"Legendary","id":30},
{"rarity":"Common","id":31},
{"rarity":"Rare","id":32},
{"rarity":"Rare","id":33},
{"rarity":"Common","id":34},
{"rarity":"Common","id":35},
{"rarity":"Rare","id":36},
{"rarity":"Rare","id":37},
{"rarity":"Common","id":38},
{"rarity":"Common","id":39},
{"rarity":"Common","id":40},
{"rarity":"Common","id":41},
{"rarity":"Common","id":42},
{"rarity":"Common","id":43},
{"rarity":"Common","id":44},
{"rarity":"Common","id":45},
{"rarity":"Common","id":46},
{"rarity":"Common","id":47},
{"rarity":"Common","id":48},
{"rarity":"Common","id":49},
{"rarity":"Common","id":50},
{"rarity":"Common","id":51},
{"rarity":"Common","id":52},
{"rarity":"Common","id":53},
{"rarity":"Common","id":54},
{"rarity":"Common","id":55},
{"rarity":"Common","id":56},
{"rarity":"Common","id":57},
{"rarity":"Common","id":58},
{"rarity":"Common","id":59},
{"rarity":"Common","id":60},
{"rarity":"Common","id":61},
{"rarity":"Common","id":62},
{"rarity":"Common","id":63},
{"rarity":"Common","id":64},
{"rarity":"Common","id":65},
{"rarity":"Common","id":66},
{"rarity":"Common","id":67},
{"rarity":"Common","id":68},
{"rarity":"Common","id":69},
{"rarity":"Common","id":70},
{"rarity":"Common","id":71},
{"rarity":"Rare","id":72},
{"rarity":"Common","id":73},
{"rarity":"Common","id":74},
{"rarity":"Common","id":75},
{"rarity":"Common","id":76},
{"rarity":"Common","id":77},
{"rarity":"Common","id":78},
{"rarity":"Rare","id":79},
{"rarity":"Common","id":80},
{"rarity":"Common","id":81},
{"rarity":"Common","id":82},
{"rarity":"Common","id":83},
{"rarity":"Common","id":84},
{"rarity":"Rare","id":85},
{"rarity":"Common","id":86},
{"rarity":"Common","id":87},
{"rarity":"Rare","id":88},
{"rarity":"Common","id":89},
{"rarity":"Common","id":90},
{"rarity":"Common","id":91},
{"rarity":"Common","id":92},
{"rarity":"Rare","id":93},
{"rarity":"Common","id":94},
{"rarity":"Rare","id":95},
{"rarity":"Common","id":96},
{"rarity":"Common","id":97},
{"rarity":"Common","id":98},
{"rarity":"Common","id":99},
{"rarity":"Common","id":100},
{"rarity":"Common","id":101},
{"rarity":"Common","id":102},
{"rarity":"Rare","id":103},
{"rarity":"Common","id":104},
{"rarity":"Legendary","id":105},
{"rarity":"Common","id":106},
{"rarity":"Common","id":107},
{"rarity":"Common","id":108},
{"rarity":"Common","id":109},
{"rarity":"Common","id":110},
{"rarity":"Common","id":111},
{"rarity":"Common","id":112},
{"rarity":"Common","id":113},
{"rarity":"Common","id":114},
{"rarity":"Common","id":115},
{"rarity":"Rare","id":116},
{"rarity":"Rare","id":117},
{"rarity":"Common","id":118},
{"rarity":"Common","id":119},
{"rarity":"Common","id":120},
{"rarity":"Common","id":121},
{"rarity":"Rare","id":122},
{"rarity":"Common","id":123},
{"rarity":"Common","id":124},
{"rarity":"Common","id":125},
{"rarity":"Common","id":126},
{"rarity":"Rare","id":127},
{"rarity":"Common","id":128},
{"rarity":"Rare","id":129},
{"rarity":"Common","id":130},
{"rarity":"Legendary","id":131},
{"rarity":"Common","id":132},
{"rarity":"Rare","id":133},
{"rarity":"Common","id":134},
{"rarity":"Common","id":135},
{"rarity":"Common","id":136},
{"rarity":"Common","id":137},
{"rarity":"Common","id":138},
{"rarity":"Common","id":139},
{"rarity":"Rare","id":140},
{"rarity":"Common","id":141},
{"rarity":"Common","id":142},
{"rarity":"Common","id":143},
{"rarity":"Common","id":144},
{"rarity":"Rare","id":145},
{"rarity":"Common","id":146},
{"rarity":"Common","id":147},
{"rarity":"Common","id":148},
{"rarity":"Common","id":149},
{"rarity":"Common","id":150},
{"rarity":"Common","id":151},
{"rarity":"Common","id":152},
{"rarity":"Common","id":153},
{"rarity":"Legendary","id":154},
{"rarity":"Common","id":155},
{"rarity":"Common","id":156},
{"rarity":"Common","id":157},
{"rarity":"Common","id":158},
{"rarity":"Common","id":159},
{"rarity":"Rare","id":160},
{"rarity":"Common","id":161},
{"rarity":"Common","id":162},
{"rarity":"Common","id":163},
{"rarity":"Common","id":164},
{"rarity":"Common","id":165},
{"rarity":"Common","id":166},
{"rarity":"Common","id":167},
{"rarity":"Common","id":168},
{"rarity":"Common","id":169},
{"rarity":"Rare","id":170},
{"rarity":"Common","id":171},
{"rarity":"Common","id":172},
{"rarity":"Common","id":173},
{"rarity":"Common","id":174},
{"rarity":"Rare","id":175},
{"rarity":"Common","id":176},
{"rarity":"Common","id":177},
{"rarity":"Common","id":178},
{"rarity":"Common","id":179},
{"rarity":"Common","id":180},
{"rarity":"Common","id":181},
{"rarity":"Common","id":182},
{"rarity":"Common","id":183},
{"rarity":"Common","id":184},
{"rarity":"Common","id":185},
{"rarity":"Common","id":186},
{"rarity":"Common","id":187},
{"rarity":"Common","id":188},
{"rarity":"Common","id":189},
{"rarity":"Common","id":190},
{"rarity":"Common","id":191},
{"rarity":"Common","id":192},
{"rarity":"Common","id":193},
{"rarity":"Common","id":194},
{"rarity":"Common","id":195},
{"rarity":"Common","id":196},
{"rarity":"Common","id":197},
{"rarity":"Common","id":198},
{"rarity":"Common","id":199},
{"rarity":"Common","id":200},
{"rarity":"Common","id":201},
{"rarity":"Common","id":202},
{"rarity":"Common","id":203},
{"rarity":"Common","id":204},
{"rarity":"Common","id":205},
{"rarity":"Common","id":206},
{"rarity":"Common","id":207},
{"rarity":"Rare","id":208},
{"rarity":"Common","id":209},
{"rarity":"Common","id":210},
{"rarity":"Common","id":211},
{"rarity":"Common","id":212},
{"rarity":"Common","id":213},
{"rarity":"Common","id":214},
{"rarity":"Common","id":215},
{"rarity":"Common","id":216},
{"rarity":"Common","id":217},
{"rarity":"Common","id":218},
{"rarity":"Common","id":219},
{"rarity":"Common","id":220},
{"rarity":"Common","id":221},
{"rarity":"Common","id":222},
{"rarity":"Common","id":223},
{"rarity":"Common","id":224},
{"rarity":"Common","id":225},
{"rarity":"Common","id":226},
{"rarity":"Common","id":227},
{"rarity":"Common","id":228},
{"rarity":"Common","id":229},
{"rarity":"Common","id":230},
{"rarity":"Common","id":231},
{"rarity":"Rare","id":232},
{"rarity":"Common","id":233},
{"rarity":"Common","id":234},
{"rarity":"Common","id":235},
{"rarity":"Legendary","id":236},
{"rarity":"Common","id":237},
{"rarity":"Rare","id":238},
{"rarity":"Common","id":239},
{"rarity":"Common","id":240},
{"rarity":"Common","id":241},
{"rarity":"Common","id":242},
{"rarity":"Common","id":243},
{"rarity":"Common","id":244},
{"rarity":"Common","id":245},
{"rarity":"Common","id":246},
{"rarity":"Common","id":247},
{"rarity":"Common","id":248},
{"rarity":"Common","id":249},
{"rarity":"Common","id":250},
{"rarity":"Common","id":251},
{"rarity":"Rare","id":252},
{"rarity":"Rare","id":253},
{"rarity":"Common","id":254},
{"rarity":"Common","id":255},
{"rarity":"Common","id":256},
{"rarity":"Common","id":257},
{"rarity":"Rare","id":258},
{"rarity":"Common","id":259},
{"rarity":"Common","id":260},
{"rarity":"Rare","id":261},
{"rarity":"Common","id":262},
{"rarity":"Rare","id":263},
{"rarity":"Common","id":264},
{"rarity":"Common","id":265},
{"rarity":"Common","id":266},
{"rarity":"Common","id":267},
{"rarity":"Common","id":268},
{"rarity":"Common","id":269},
{"rarity":"Common","id":270},
{"rarity":"Rare","id":271},
{"rarity":"Common","id":272},
{"rarity":"Common","id":273},
{"rarity":"Common","id":274},
{"rarity":"Common","id":275},
{"rarity":"Common","id":276},
{"rarity":"Common","id":277},
{"rarity":"Common","id":278},
{"rarity":"Common","id":279},
{"rarity":"Common","id":280},
{"rarity":"Common","id":281},
{"rarity":"Common","id":282},
{"rarity":"Common","id":283},
{"rarity":"Common","id":284},
{"rarity":"Common","id":285},
{"rarity":"Common","id":286},
{"rarity":"Common","id":287},
{"rarity":"Common","id":288},
{"rarity":"Common","id":289},
{"rarity":"Common","id":290},
{"rarity":"Common","id":291},
{"rarity":"Common","id":292},
{"rarity":"Common","id":293},
{"rarity":"Common","id":294},
{"rarity":"Common","id":295},
{"rarity":"Common","id":296},
{"rarity":"Common","id":297},
{"rarity":"Common","id":298},
{"rarity":"Common","id":299},
{"rarity":"Common","id":300},
{"rarity":"Common","id":301},
{"rarity":"Common","id":302},
{"rarity":"Common","id":303},
{"rarity":"Rare","id":304},
{"rarity":"Common","id":305},
{"rarity":"Common","id":306},
{"rarity":"Common","id":307},
{"rarity":"Common","id":308},
{"rarity":"Common","id":309},
{"rarity":"Common","id":310},
{"rarity":"Common","id":311},
{"rarity":"Common","id":312},
{"rarity":"Common","id":313},
{"rarity":"Rare","id":314},
{"rarity":"Common","id":315},
{"rarity":"Common","id":316},
{"rarity":"Common","id":317},
{"rarity":"Common","id":318},
{"rarity":"Common","id":319},
{"rarity":"Common","id":320},
{"rarity":"Common","id":321},
{"rarity":"Common","id":322},
{"rarity":"Rare","id":323},
{"rarity":"Common","id":324},
{"rarity":"Common","id":325},
{"rarity":"Common","id":326},
{"rarity":"Common","id":327},
{"rarity":"Common","id":328},
{"rarity":"Common","id":329},
{"rarity":"Common","id":330},
{"rarity":"Common","id":331},
{"rarity":"Common","id":332},
{"rarity":"Common","id":333},
{"rarity":"Common","id":334},
{"rarity":"Rare","id":335},
{"rarity":"Common","id":336},
{"rarity":"Common","id":337},
{"rarity":"Common","id":338},
{"rarity":"Common","id":339},
{"rarity":"Common","id":340},
{"rarity":"Common","id":341},
{"rarity":"Common","id":342},
{"rarity":"Common","id":343},
{"rarity":"Common","id":344},
{"rarity":"Common","id":345},
{"rarity":"Common","id":346},
{"rarity":"Common","id":347},
{"rarity":"Common","id":348},
{"rarity":"Common","id":349},
{"rarity":"Common","id":350},
{"rarity":"Common","id":351},
{"rarity":"Common","id":352},
{"rarity":"Common","id":353},
{"rarity":"Common","id":354},
{"rarity":"Common","id":355},
{"rarity":"Common","id":356},
{"rarity":"Common","id":357},
{"rarity":"Rare","id":358},
{"rarity":"Common","id":359},
{"rarity":"Common","id":360},
{"rarity":"Common","id":361},
{"rarity":"Common","id":362},
{"rarity":"Rare","id":363},
{"rarity":"Common","id":364},
{"rarity":"Common","id":365},
{"rarity":"Common","id":366},
{"rarity":"Common","id":367},
{"rarity":"Common","id":368},
{"rarity":"Rare","id":369},
{"rarity":"Common","id":370},
{"rarity":"Common","id":371},
{"rarity":"Common","id":372},
{"rarity":"Common","id":373},
{"rarity":"Common","id":374},
{"rarity":"Rare","id":375},
{"rarity":"Common","id":376},
{"rarity":"Common","id":377},
{"rarity":"Common","id":378},
{"rarity":"Common","id":379},
{"rarity":"Common","id":380},
{"rarity":"Common","id":381},
{"rarity":"Common","id":382},
{"rarity":"Common","id":383},
{"rarity":"Common","id":384},
{"rarity":"Common","id":385},
{"rarity":"Common","id":386},
{"rarity":"Common","id":387},
{"rarity":"Common","id":388},
{"rarity":"Common","id":389},
{"rarity":"Common","id":390},
{"rarity":"Common","id":391},
{"rarity":"Common","id":392},
{"rarity":"Common","id":393},
{"rarity":"Rare","id":394},
{"rarity":"Common","id":395},
{"rarity":"Common","id":396},
{"rarity":"Common","id":397},
{"rarity":"Common","id":398},
{"rarity":"Common","id":399},
{"rarity":"Common","id":400},
{"rarity":"Rare","id":401},
{"rarity":"Common","id":402},
{"rarity":"Common","id":403},
{"rarity":"Common","id":404},
{"rarity":"Rare","id":405},
{"rarity":"Common","id":406},
{"rarity":"Common","id":407},
{"rarity":"Common","id":408},
{"rarity":"Common","id":409},
{"rarity":"Common","id":410},
{"rarity":"Common","id":411},
{"rarity":"Common","id":412},
{"rarity":"Common","id":413},
{"rarity":"Common","id":414},
{"rarity":"Common","id":415},
{"rarity":"Common","id":416},
{"rarity":"Common","id":417},
{"rarity":"Legendary","id":418},
{"rarity":"Common","id":419},
{"rarity":"Common","id":420},
{"rarity":"Common","id":421},
{"rarity":"Common","id":422},
{"rarity":"Rare","id":423},
{"rarity":"Common","id":424},
{"rarity":"Rare","id":425},
{"rarity":"Common","id":426},
{"rarity":"Common","id":427},
{"rarity":"Common","id":428},
{"rarity":"Rare","id":429},
{"rarity":"Common","id":430},
{"rarity":"Rare","id":431},
{"rarity":"Common","id":432},
{"rarity":"Rare","id":433},
{"rarity":"Rare","id":434},
{"rarity":"Common","id":435},
{"rarity":"Common","id":436},
{"rarity":"Common","id":437},
{"rarity":"Common","id":438},
{"rarity":"Common","id":439},
{"rarity":"Common","id":440},
{"rarity":"Common","id":441},
{"rarity":"Rare","id":442},
{"rarity":"Common","id":443},
{"rarity":"Common","id":444},
{"rarity":"Common","id":445},
{"rarity":"Common","id":446},
{"rarity":"Common","id":447},
{"rarity":"Common","id":448},
{"rarity":"Common","id":449},
{"rarity":"Common","id":450},
{"rarity":"Common","id":451},
{"rarity":"Common","id":452},
{"rarity":"Common","id":453},
{"rarity":"Common","id":454},
{"rarity":"Common","id":455},
{"rarity":"Common","id":456},
{"rarity":"Common","id":457},
{"rarity":"Common","id":458},
{"rarity":"Common","id":459},
{"rarity":"Common","id":460},
{"rarity":"Common","id":461},
{"rarity":"Common","id":462},
{"rarity":"Common","id":463},
{"rarity":"Common","id":464},
{"rarity":"Common","id":465},
{"rarity":"Common","id":466},
{"rarity":"Common","id":467},
{"rarity":"Common","id":468},
{"rarity":"Common","id":469},
{"rarity":"Common","id":470},
{"rarity":"Common","id":471},
{"rarity":"Common","id":472},
{"rarity":"Common","id":473},
{"rarity":"Common","id":474},
{"rarity":"Common","id":475},
{"rarity":"Common","id":476},
{"rarity":"Common","id":477},
{"rarity":"Common","id":478},
{"rarity":"Common","id":479},
{"rarity":"Common","id":480},
{"rarity":"Common","id":481},
{"rarity":"Common","id":482},
{"rarity":"Common","id":483},
{"rarity":"Common","id":484},
{"rarity":"Common","id":485},
{"rarity":"Common","id":486},
{"rarity":"Common","id":487},
{"rarity":"Common","id":488},
{"rarity":"Common","id":489},
{"rarity":"Common","id":490},
{"rarity":"Common","id":491},
{"rarity":"Legendary","id":492},
{"rarity":"Common","id":493},
{"rarity":"Common","id":494},
{"rarity":"Legendary","id":495},
{"rarity":"Legendary","id":496},
{"rarity":"Common","id":497},
{"rarity":"Common","id":498},
{"rarity":"Common","id":499},
{"rarity":"Legendary","id":500}
]

14
configs/nft_rarity.json Normal file
View File

@ -0,0 +1,14 @@
[
{
"rarity": "Common",
"ticket": 1
},
{
"rarity": "Rare",
"ticket": 2
},
{
"rarity": "Legendary",
"ticket": 5
}
]

View File

@ -64,6 +64,13 @@
1. 增加验证google的access token(32) 1. 增加验证google的access token(32)
2. 增加两种任务类型: GoogleConnect, GameAchievement 2. 增加两种任务类型: GoogleConnect, GameAchievement
#### 20240508
1. 用户状态接口(10) 增加字段, gameScore, gameTicket, googleId, rankGame
1. 增加接口: 游戏任务列表(33), 领取游戏任务奖励(34), 大转盘抽奖(35), 大转盘抽奖记录(36), 游戏积分详情列表(37), 游戏积分排行榜(38)
1. 社交任务活动信息(3), 增加字段drawTime, 表示转盘开始时间
1. 增加接口: 转盘配置(39), 用户NFT列表(40)
### 1. 钱包预登录 ### 1. 钱包预登录
#### Request #### Request
@ -164,6 +171,7 @@ SiweMessage的nonce说明(具体参考例子):
"autoclaim": false // 任务完成后是否自动获取奖励 "autoclaim": false // 任务完成后是否自动获取奖励
} }
], ],
"drawTime": 1702628292366, // 抽奖活动开始时间
"startTime": 1702628292366, // 活动开始时间 "startTime": 1702628292366, // 活动开始时间
"endTime": 1705220292366 // 活动结束时间 "endTime": 1705220292366 // 活动结束时间
} }
@ -186,7 +194,7 @@ SiweMessage的nonce说明(具体参考例子):
```js ```js
[ [
{ {
"status": 2, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 9: 失败 "status": 2, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取, 9: 失败
"id": "TwitterConnect", // 任务id "id": "TwitterConnect", // 任务id
"timeStart": 1703150269527, // 任务开始时间 "timeStart": 1703150269527, // 任务开始时间
"data": { // 当前任务带的额外信息, 比如twitter的id和昵称等 "data": { // 当前任务带的额外信息, 比如twitter的id和昵称等
@ -357,10 +365,14 @@ body:
"twitterName": "", "twitterName": "",
"twitterAvatar": "", // twitter头像 "twitterAvatar": "", // twitter头像
"discordId": "", "discordId": "",
"googleId": "",
"discordName": "", "discordName": "",
"scoreToday": 100, // 今日获得积分 "scoreToday": 100, // 今日获得积分
"scoreTotal": 200, // 总积分 "scoreTotal": 200, // 总积分
"gameScore": 100, // 游戏内积分
"gameTicket": 1, // 游戏内可抽奖次数
"rankTotal": "-", "rankTotal": "-",
"rankGame": "", // 游戏内积分排行榜
"invite": "邀请人address", "invite": "邀请人address",
"inviteCount": 0, // 我邀请的用户总数 "inviteCount": 0, // 我邀请的用户总数
"inviteScore": 0, // 我邀请用户总数获得的分数 "inviteScore": 0, // 我邀请用户总数获得的分数
@ -777,7 +789,7 @@ body:
``` ```
### 26.\ 宝箱助力状态查询 ### 26. 宝箱助力状态查询
#### Request #### Request
@ -992,3 +1004,208 @@ body:
{ {
} }
``` ```
### 33. 游戏任务列表
#### Request
- URL`/api/ingame/tasks`
- 方法:`GET`
#### Response
```js
[
{
"id": "任务id",
"task": "任务类型",
"title": "任务名",
"desc": "任务描述",
"show": 1, // 是否显示, 0: 不显示, 1: 显示
"type": 1, //任务类型, 1: 一次性任务, 2: 日常任务
"pretasks": ["task id 1"], //前置任务
"score": 0, // 完成任务可获得的积分
"ticket": 0, // 完成任务可获得抽奖次数
"cfg": {}, // 其他一些任务相关配置参数, 比如icon, 或者其他未考虑的参数
"end": false, // 是否已经结束
"status": 1,// 任务状态, -1:不可用 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取, 9: 失败
"autoclaim": false // 任务完成后是否自动获取奖励
}
]
```
### 34.\* 领取游戏任务奖励
#### Request
- URL`/api/ingame/claim`
- 方法POST
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"task": "g31x9wzja7eg18t3vtm" // 任务id
}
```
#### Response
```json
{
"status": 1, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取 9: 失败
"score": 1,
"ticket": 3, // 获得的ticket, 可能没这个字段
}
```
### 35.\* 大转盘抽奖
#### Request
- URL`/api/ingame/draw`
- 方法POST
- 头部:
- Authorization: Bearer JWT_token
body:
```js
{
"step": 1
}
```
#### Response
```json
{
score: 100, // 获得的积分
items: [
{
id: "001", // 物品id
type: 1, // 1白单, 2: nft
name: "", // 物品名
desc: "", // 描述
amount: 1 // 数量
}
]
}
```
### 36.\* 大转盘抽奖记录
#### Request
- URL`/api/ingame/draw_history`
- 方法GET
- 头部:
- Authorization: Bearer JWT_token
#### Response
```json
[{
time: 12123, // 抽奖时间
score: 100, // 获得的积分
items: [
{
id: "001", // 物品id
type: 1, // 1白单, 2: nft
name: "", // 物品名
desc: "", // 描述
amount: 1 // 数量
}
],
position: [1, 2] //每次转动的位置
}]
```
### 37.\* 游戏积分详情列表
#### Request
- URL`/api/ingame/score_list`
- 方法:`GET`
- 头部:
- Authorization: Bearer JWT_token
#### Response
```js
[{
score: 100, // 获得的积分
type: '', // 获取原因
time: 111 // 开启时间
}]
```
### 38. 游戏 积分排行榜
#### Request
- URL`/api/ingame/leaderboard/:page`
- 方法:`GET`
- 参数:
- `page` (返回数据的分页序号, 0 开始)
> 默认返回100条记录, 如果要返回不同数量, query param传 limit
#### Response
```json
[
{
"rank": 1, // 排名, 从1开始
"level": 1, // 段位
"nickname": "昵称",
"score": 1 //获得的积分
}
]
```
### 39. 转盘配置
#### Request
- URL`/api/ingame/draw_cfg`
- 方法:`GET`
#### Response
```json
[
{
"position": 1, // 位置, 从1开始
"type": 0, // 类型, 0: 积分, 1: 白单, 2: nft
"amount": 1, // 数量
"name": "", // 没有的话, 就是score
}
]
```
### 40. \* 用户NFT列表
#### Request
- URL`/api/ingame/nft_list`
- 方法:`GET`
#### Response
```json
[
{
"tokenId": "1", // tokenID
"rarity": "", // Common, Rare, Legendary
}
]
```

View File

@ -293,37 +293,71 @@
"params": { "time": 6, "failRate": 0 } "params": { "time": 6, "failRate": 0 }
}, },
{ {
"id": "e2yhq2lj30vwcpedv8p", "id": "e2djzxndhjyuc7xadqj",
"task": "GoogleConnect", "task": "TwitterRetweet",
"title": "Connect your game account", "title": "RT - Canoe Gameplay",
"type": 1, "type": 1,
"desc": "Please connect your game account", "desc": "Retweet specific tweets",
"score": 100,
"category": "Social Tasks", "category": "Social Tasks",
"score": 50,
"autoclaim": false, "autoclaim": false,
"cfg": { "icon": "google" }, "pretasks": ["e2yhq2lj30vwcpedv7p"],
"cfg": { "icon": "twitter", "content": "1788592368855478534"},
"start": "2024-01-01 00:00", "start": "2024-01-01 00:00",
"end": "2025-01-01 00:00", "end": "2025-01-01 00:00",
"checkChain": false, "checkChain": false,
"params": {} "params": { "time": 6, "failRate": 0 }
}, },
{ {
"id": "e2fclylj30vwcpe1szl", "id": "e2gefd7432pcxrrhjhz",
"task": "GameAchievement", "task": "TwitterLike",
"title": "Receive Game Achievement", "title": "Like - Canoe Gameplay",
"type": 1, "type": 1,
"desc": "Receive Game Achievement", "desc": "Like specific tweets",
"category": "Social Tasks", "category": "Social Tasks",
"score": 100, "score": 50,
"autoclaim": false, "autoclaim": false,
"pretasks": ["e2yhq2lj30vwcpedv8p"], "pretasks": ["e2yhq2lj30vwcpedv7p"],
"cfg": { }, "cfg": { "icon": "twitter", "content": "1788592368855478534" },
"start": "2024-01-01 00:00",
"end": "2025-01-01 00:00",
"checkChain": false,
"params": { "time": 6, "failRate": 0 }
},
{
"id": "e286nywaz7cmdf7keq5",
"task": "TwitterRetweet",
"title": "RT - Play-to-Airdrop Coming Soon",
"type": 1,
"desc": "Retweet specific tweets",
"category": "Social Tasks",
"score": 50,
"autoclaim": false,
"pretasks": ["e2yhq2lj30vwcpedv7p"],
"cfg": { "icon": "twitter", "content": "1788919520104681866"},
"start": "2024-01-01 00:00",
"end": "2025-01-01 00:00",
"checkChain": false,
"params": { "time": 6, "failRate": 0 }
},
{
"id": "e2cy5ha5nmre9w3pq2k",
"task": "TwitterLike",
"title": "Like - Play-to-Airdrop Coming Soon",
"type": 1,
"desc": "Like specific tweets",
"category": "Social Tasks",
"score": 50,
"autoclaim": false,
"pretasks": ["e2yhq2lj30vwcpedv7p"],
"cfg": { "icon": "twitter", "content": "1788919520104681866" },
"start": "2024-01-01 00:00", "start": "2024-01-01 00:00",
"end": "2025-01-01 00:00", "end": "2025-01-01 00:00",
"checkChain": false, "checkChain": false,
"params": { "time": 6, "failRate": 0 } "params": { "time": 6, "failRate": 0 }
} }
], ],
"drawTime": 1715245160457,
"startTime": 1713355200000, "startTime": 1713355200000,
"endTime": 1715903999999 "endTime": 1715903999999
} }

View File

@ -266,7 +266,37 @@
"cfg": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"}, "cfg": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"},
"score": 100, "score": 100,
"params": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"} "params": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"}
}, { },{
"id": "e2yhq2lj30vwcpedv8p",
"task": "GoogleConnect",
"title": "Connect your game account",
"type": 1,
"desc": "Please connect your game account",
"score": 100,
"category": "Social Tasks",
"autoclaim": false,
"cfg": { "icon": "google" },
"start": "2024-01-01 00:00",
"end": "2025-01-01 00:00",
"checkChain": false,
"params": {}
},
{
"id": "e2fclylj30vwcpe1szl",
"task": "GameAchievement",
"title": "Receive Game Achievement",
"type": 1,
"desc": "Receive Game Achievement",
"category": "Social Tasks",
"score": 100,
"autoclaim": false,
"pretasks": ["e2yhq2lj30vwcpedv8p"],
"cfg": { },
"start": "2024-01-01 00:00",
"end": "2025-01-01 00:00",
"checkChain": false,
"params": { "time": 6, "failRate": 0 }
}, {
"id": "e2f7t4lj33vwcpe0ldr", "id": "e2f7t4lj33vwcpe0ldr",
"task": "BurnNft", "task": "BurnNft",
"type": 1, "type": 1,

View File

@ -17,6 +17,8 @@
"repairredis": "ts-node -r tsconfig-paths/register src/repairredis.ts", "repairredis": "ts-node -r tsconfig-paths/register src/repairredis.ts",
"checkredis": "ts-node -r tsconfig-paths/register src/checkredis.ts", "checkredis": "ts-node -r tsconfig-paths/register src/checkredis.ts",
"additem": "ts-node -r tsconfig-paths/register src/addboxdata.ts", "additem": "ts-node -r tsconfig-paths/register src/addboxdata.ts",
"taskid": "ts-node -r tsconfig-paths/register src/generateTaskId.ts",
"token": "ts-node -r tsconfig-paths/register src/generateToken.ts",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test": "jest" "test": "jest"
}, },

View File

@ -9,6 +9,7 @@ import logger from 'logger/logger'
import NonceRecordSchedule from 'schedule/noncerecord.schedule' import NonceRecordSchedule from 'schedule/noncerecord.schedule'
import { RouterMap, ZRedisClient } from 'zutils' import { RouterMap, ZRedisClient } from 'zutils'
import { SyncLocker } from 'common/SyncLocker' import { SyncLocker } from 'common/SyncLocker'
import CacheSchedule from 'schedule/cache.schedule'
const zReqParserPlugin = require('plugins/zReqParser') const zReqParserPlugin = require('plugins/zReqParser')
@ -116,7 +117,7 @@ export class ApiServer {
logger.log('REDIS Connected') logger.log('REDIS Connected')
} }
private initSchedules() { private initSchedules() {
// new CacheSchedule().scheduleAll() new CacheSchedule().scheduleAll()
new NonceRecordSchedule().scheduleAll() new NonceRecordSchedule().scheduleAll()
} }

View File

@ -68,3 +68,8 @@ export const BASE52_ALPHABET = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV
// 检查role时默认的工会id // 检查role时默认的工会id
export const DEFAULT_GUILD = '930002266868555827' export const DEFAULT_GUILD = '930002266868555827'
// 游戏内积分排行榜key
export const INGAME_SCORE_KEY = 'ingame:score'
export const ACTIVITY_NAME = 'uaw_activity'

View File

@ -0,0 +1,14 @@
import { singleton } from 'zutils'
@singleton
export class ReadOnlyCache {
map: Map<string, any> = new Map()
public getData(key: string) {
return this.map.get(key)
}
public setData(key: string, value: any) {
this.map.set(key, value)
}
}

View File

@ -9,6 +9,7 @@ import { formatNumShow, isValidShareCode } from 'common/Utils'
import { checkReCaptcha } from 'services/google.svr' import { checkReCaptcha } from 'services/google.svr'
import { RANK_SCORE_SCALE, SCORE_INVITE_REBATE } from 'common/Constants' import { RANK_SCORE_SCALE, SCORE_INVITE_REBATE } from 'common/Constants'
import logger from 'logger/logger' import logger from 'logger/logger'
import { ReadOnlyCache } from 'common/ReadOnlyCache'
const MAX_LIMIT = 100 const MAX_LIMIT = 100
export default class ActivityController extends BaseController { export default class ActivityController extends BaseController {
@ -19,7 +20,13 @@ export default class ActivityController extends BaseController {
if (!id) { if (!id) {
throw new ZError(11, 'invalid activity id') throw new ZError(11, 'invalid activity id')
} }
let activity = await ActivityInfo.findById(id) let activity = new ReadOnlyCache().getData(id)
if (!activity) {
activity = await ActivityInfo.findById(id)
if (activity) {
new ReadOnlyCache().setData(id, activity)
}
}
if (!activity) { if (!activity) {
throw new ZError(12, 'activity not found') throw new ZError(12, 'activity not found')
} }
@ -93,7 +100,7 @@ export default class ActivityController extends BaseController {
const totalKey = rankKey(user.activity) const totalKey = rankKey(user.activity)
const keyInvite = `${user.activity}:invite` const keyInvite = `${user.activity}:invite`
let users = await ActivityUser.find({ inviteUser: user.id }) let users = await ActivityUser.find({ inviteUser: user.id })
let records = await ScoreRecord.find({ type: SCORE_INVITE_REBATE, user: user.id, activity: user.activity }) let records = await ScoreRecord.find({ user: user.id, activity: user.activity, type: SCORE_INVITE_REBATE })
let scoreMap = new Map() let scoreMap = new Map()
for (let r of records) { for (let r of records) {
if (!scoreMap.has(r.data.fromUser)) { if (!scoreMap.has(r.data.fromUser)) {
@ -126,7 +133,7 @@ export default class ActivityController extends BaseController {
async scoreList(req) { async scoreList(req) {
let user = req.user let user = req.user
const records = await ScoreRecord.find({ user: user.id }).sort({ const records = await ScoreRecord.find({ user: user.id }).sort({
_id: -1, createdAt: -1,
}) })
return records.map(record => { return records.map(record => {
return { return {

View File

@ -0,0 +1,292 @@
import { ACTIVITY_NAME, INGAME_SCORE_KEY, RANK_SCORE_SCALE } from 'common/Constants'
import { SyncLocker } from 'common/SyncLocker'
import { formatNumShow } from 'common/Utils'
import logger from 'logger/logger'
import { join } from 'path'
import { ActivityInfo } from 'models/ActivityInfo'
import { ActivityUser } from 'models/ActivityUser'
import { DrawRecord } from 'models/DrawRecord'
import { InGameScoreRecord } from 'models/InGameScoreRecord'
import { InGameTaskRecord } from 'models/InGameTaskRecord'
import { rankLevel } from 'services/rank.svr'
import { formatDate } from 'utils/utcdate.util'
import { BaseController, ROLE_ANON, ZError, ZRedisClient, role, router } from 'zutils'
import { formatAddress } from 'zutils/utils/chain.util'
import { InGameStats } from 'models/InGameStats'
import { drawCfgs, drawOnce, queryInGameInfo } from 'services/game.svr'
import { updateRankScore } from 'services/ingame.rank.svr'
import { NftHolder } from 'models/chain/NftHolder'
import { tokenRarity } from 'taskingame/NftHolderCheck'
const fs = require('fs')
const sourceList = require('../../configs/ingame_tasks.json')
const taskMap = new Map()
for (let o of sourceList) {
taskMap.set(o.id, o)
}
const taskListStr = JSON.stringify(sourceList)
const initTasks = () => {
const tasks = join(__dirname, '../taskingame')
const list = new Map()
fs.readdirSync(tasks)
.filter((file: string) => ~file.search(/^[^.].*\.(ts|js)$/))
.forEach((file: any) => {
const Task = require('../taskingame/' + file)
list.set(Task.default.name, Task)
})
return list
}
const allTasks = initTasks()
/**
*
*/
const MAX_LIMIT = 100
class InGameController extends BaseController {
/**
*
*/
@role(ROLE_ANON)
@router('get /api/ingame/tasks')
async taskList(req) {
const user = req.user
const now = Date.now()
let activity = req.activity
let list = JSON.parse(taskListStr)
let gameData = {
loginVal: '0',
battleTimes: 0,
winTimes: 0,
kills: 0,
getGoldVal: '0',
}
for (let o of list) {
if (o.checkTime) {
o.status = now < activity.drawTime ? -1 : 0
} else {
o.status = 0
}
}
if (user) {
if (user.googleId) {
let res = await queryInGameInfo(user.googleId, '0')
Object.assign(gameData, res)
}
const dateTag = formatDate(new Date())
let records = await InGameTaskRecord.find({ user: user.id, dateTag })
let recordSet = new Set()
for (let record of records) {
recordSet.add(record.task)
}
for (let sub of list) {
const Task = allTasks.get(sub.task)
const taskInstance = new Task.default({ user, activity: req.activity })
if (sub.status >= 0 && (await taskInstance.check(sub, gameData))) {
sub.status = 2
if (sub.task === 'GoogleConnect') {
sub.status = 3
}
}
sub.status = recordSet.has(sub.id) ? 3 : sub.status === 0 ? 1 : sub.status
}
}
return list
}
/**
*
*/
@router('post /api/ingame/claim')
async claimInGameReward(req) {
await new SyncLocker().checkLock(req)
logger.db('claim_ingame', req)
const { task } = req.params
const user = req.user
if (!user.googleId) {
throw new ZError(10, 'need connect game account first')
}
if (!taskMap.has(task)) {
throw new ZError(11, 'task not found')
}
let cfg = taskMap.get(task)
if (!allTasks.has(cfg.task)) {
throw new ZError(12, 'task not found')
}
const dateTag = formatDate(new Date())
let record = await InGameTaskRecord.findOne({ user: user.id, task, dateTag })
if (record) {
throw new ZError(12, 'already claimed today')
}
const Task = allTasks.get(cfg.task)
const taskInstance = new Task.default({ user, activity: req.activity })
const { score, ticket } = await taskInstance.claimReward(cfg)
let recordNew = new InGameTaskRecord({
user: user.id,
task,
dateTag,
score,
ticket,
})
await recordNew.save()
return { score, ticket, status: 3 }
}
/**
*
*/
@router('post /api/ingame/draw')
async draw(req) {
await new SyncLocker().checkLock(req)
logger.db('draw_ingame', req)
const user = req.user
const { step } = req.params
if (!user.googleId) {
throw new ZError(10, 'need connect game account first')
}
const ingameStat = await InGameStats.insertOrUpdate({ user: user.id }, {})
if (ingameStat.ticket < step) {
throw new ZError(11, 'no ticket')
}
// TODO:: 抽奖逻辑
let saveRes = await InGameStats.findOneAndUpdate(
{ user: user.id, ticket: { $gte: step } },
{ $inc: { ticket: -step } },
{
new: false,
upsert: false,
includeResultMetadata: true,
},
)
// @ts-ignore
if (!saveRes.ok || saveRes.lastErrorObject.n < 1) {
throw new ZError(11, 'no ticket.')
}
let score = 0
let items: any[] = []
let position: number[] = []
const dateTag = formatDate(new Date())
for (let i = 0; i < step; i++) {
let reward = await drawOnce(user.inWhiteList)
let record = new DrawRecord({
user: user.id,
score: 0,
})
if (reward.type === 0) {
score += reward.amount
record.score = reward.amount
await updateRankScore({
user: user.id,
score: reward.amount,
scoreType: 'draw_ingame',
scoreParams: {
date: dateTag,
position: reward.position,
},
})
} else {
items.push(reward)
record.items = [reward]
if (reward.type === 1) {
user.inWhiteList = true
await user.save()
}
}
await record.save()
position.push(reward.position)
}
return { score, items, position }
}
/**
*
*/
@router('get /api/ingame/draw_history')
async history(req) {
const user = req.user
let records = await DrawRecord.find({ user: user.id }).sort({ _id: -1 })
return records.map(record => {
return {
score: formatNumShow(record.score),
// @ts-ignore
time: record.createdAt.getTime(),
items: record.items,
}
})
}
/**
*
*/
@router('get /api/ingame/score_list')
async scoreList(req) {
const user = req.user
let records = await InGameScoreRecord.find({ user: user.id }).sort({ _id: -1 })
return records.map(record => {
return {
score: formatNumShow(record.score),
// @ts-ignore
time: record.createdAt.getTime(),
type: record.type,
}
})
}
/**
*
*/
@role(ROLE_ANON)
@router('get /api/ingame/leaderboard/:page')
async leaderboard(req) {
let { page, limit } = req.params
page = parseInt(page || '0')
limit = parseInt(limit || MAX_LIMIT)
page = page < 0 ? 0 : page
const start = page * limit
const end = start + limit - 1
const records = await new ZRedisClient().zrevrange(INGAME_SCORE_KEY, start, end)
let results: any = []
for (let i = 0; i < records.length; i += 2) {
const id = records[i]
let score = formatNumShow(parseInt(records[i + 1]) / RANK_SCORE_SCALE)
const user = await ActivityUser.findById(id)
const rank = start + i / 2 + 1
results.push({
rank,
level: rankLevel(rank),
nickname: user?.twitterName || user?.discordName || user?.address ? formatAddress(user.address) : 'unknown',
avatar: user?.twitterAvatar || '',
score,
})
}
return results
}
@role(ROLE_ANON)
@router('get /api/ingame/draw_cfg')
async drawCfg(req) {
let cfgs = drawCfgs
let results = []
for (let cfg of cfgs) {
results.push({
position: cfg.position,
type: cfg.type,
amount: cfg.amount,
name: cfg.name,
})
}
return results
}
@router('get /api/ingame/nft_list')
async nftList(req) {
let user = req.user
let records = await NftHolder.find({
chain: '1',
address: '0xec23679653337d4c6390d0eeba682246a6067777',
user: user.address.toLowerCase(),
})
return records.map(record => {
return {
tokenId: record.tokenId,
rarity: tokenRarity(record.tokenId),
}
})
}
}

View File

@ -13,16 +13,17 @@ import { aesDecrypt } from 'zutils/utils/security.util'
import { base58ToHex } from 'zutils/utils/string.util' import { base58ToHex } from 'zutils/utils/string.util'
import { ActivityGame } from 'models/ActivityGame' import { ActivityGame } from 'models/ActivityGame'
import { import {
INGAME_SCORE_KEY,
MAX_ENHANCE_COUNT_ADV, MAX_ENHANCE_COUNT_ADV,
MAX_ENHANCE_COUNT_BASE, MAX_ENHANCE_COUNT_BASE,
RANK_SCORE_SCALE, RANK_SCORE_SCALE,
SCORE_INVITE_USER,
SCORE_SOCIAL_TASK, SCORE_SOCIAL_TASK,
} from 'common/Constants' } from 'common/Constants'
import { formatNumShow, isObjectIdString } from 'common/Utils' import { formatNumShow, isObjectIdString } from 'common/Utils'
import { ChestEnhanceRecord } from 'models/ChestEnhanceRecord' import { ChestEnhanceRecord } from 'models/ChestEnhanceRecord'
import logger from 'logger/logger' import logger from 'logger/logger'
import { verifyToken } from 'services/google.svr' import { verifyToken } from 'services/google.svr'
import { InGameStats } from 'models/InGameStats'
const LOGIN_TIP = 'This signature is just to verify your identity' const LOGIN_TIP = 'This signature is just to verify your identity'
@ -120,6 +121,7 @@ class SignController extends BaseController {
const totalKey = rankKey(user.activity) const totalKey = rankKey(user.activity)
const totalScore = await new ZRedisClient().zscore(totalKey, user.id) const totalScore = await new ZRedisClient().zscore(totalKey, user.id)
const totalRank = await new ZRedisClient().zrevrank(totalKey, user.id) const totalRank = await new ZRedisClient().zrevrank(totalKey, user.id)
const rankGame = await new ZRedisClient().zrevrank(INGAME_SCORE_KEY, user.id)
let invite = '' let invite = ''
if (user.inviteUser) { if (user.inviteUser) {
const inviteUser = await ActivityUser.findById(user.inviteUser) const inviteUser = await ActivityUser.findById(user.inviteUser)
@ -139,6 +141,7 @@ class SignController extends BaseController {
if (enhanceCount < 0) { if (enhanceCount < 0) {
enhanceCount = 0 enhanceCount = 0
} }
const ingameStat = await InGameStats.insertOrUpdate({ user: user.id }, {})
let result = { let result = {
address: user.address.toLowerCase(), address: user.address.toLowerCase(),
boost: user.boost || 1, boost: user.boost || 1,
@ -151,6 +154,7 @@ class SignController extends BaseController {
scoreToday: formatNumShow(todayScore ? parseInt(todayScore + '') / RANK_SCORE_SCALE : 0), scoreToday: formatNumShow(todayScore ? parseInt(todayScore + '') / RANK_SCORE_SCALE : 0),
scoreTotal: formatNumShow(totalScore ? parseInt(totalScore + '') / RANK_SCORE_SCALE : 0), scoreTotal: formatNumShow(totalScore ? parseInt(totalScore + '') / RANK_SCORE_SCALE : 0),
rankTotal: totalRank != undefined ? totalRank : '-', rankTotal: totalRank != undefined ? totalRank : '-',
rankGame: rankGame !== undefined ? rankGame : '-',
invite, invite,
inviteCount: records.length, inviteCount: records.length,
inviteScore, inviteScore,
@ -159,6 +163,10 @@ class SignController extends BaseController {
mapopen: gameRecord.status, mapopen: gameRecord.status,
enhanceCount, enhanceCount,
inWhiteList: user.inWhiteList ? 1 : 0, inWhiteList: user.inWhiteList ? 1 : 0,
googleId: user.googleId,
googleMail: user.googleEmail,
gameScore: ingameStat.score,
gameTicket: ingameStat.ticket,
} }
return result return result
} }

19
src/generateTaskId.ts Normal file
View File

@ -0,0 +1,19 @@
import mongoose from 'mongoose'
import * as dotenv from 'dotenv'
const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development'
dotenv.config({ path: envFile })
console.log(process.env.DB_MAIN)
import { customAlphabet } from 'nanoid'
const BASE52_ALPHABET = '123456789abcdefghjkmnpqrstuvwxyz'
;(async () => {
try {
for (let i = 0; i < 20; i++) {
let code = 'g3' + customAlphabet(BASE52_ALPHABET, 17)()
console.log(code)
}
} catch (e) {
console.log(e)
}
process.exit(0)
})()

30
src/generateToken.ts Normal file
View File

@ -0,0 +1,30 @@
import mongoose from 'mongoose'
import * as dotenv from 'dotenv'
const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development'
dotenv.config({ path: envFile })
console.log(process.env.DB_MAIN)
import { createSigner } from 'fast-jwt'
import { ActivityUser } from 'models/ActivityUser'
;(async () => {
try {
const signSync = createSigner({
key: process.env.API_TOKEN_SECRET,
})
const address = '0x50A8e60041A206AcaA5F844a1104896224be6F39'
const user = await ActivityUser.findOne({ address })
if (!user) {
console.log('User not found')
process.exit(0)
}
const token = signSync({
id: user.id,
address,
activity: 'uaw_activity',
})
console.log(token)
} catch (e) {
console.log(e)
}
process.exit(0)
})()

View File

@ -89,6 +89,9 @@ export class ActivityInfoClass extends BaseModule {
@prop({ required: false }) @prop({ required: false })
public pause: boolean public pause: boolean
@prop()
public drawTime: number
@prop() @prop()
public startTime: number public startTime: number

47
src/models/DrawRecord.ts Normal file
View File

@ -0,0 +1,47 @@
import { dbconn } from 'decorators/dbconn'
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { BaseModule } from './Base'
@modelOptions({ schemaOptions: { _id: false } })
export class Item {
@prop()
id: string
@prop()
type: number
@prop()
name: string
@prop({ default: 1 })
amount: number
}
/**
*
*/
@dbconn()
@index({ user: 1 }, { unique: false })
@index({ user: 1, dateTag: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'draw_record', timestamps: true },
})
class DrawRecordClass extends BaseModule {
@prop()
public user: string
@prop()
public dateTag: string
@prop()
public position: number
// 获得的游戏内积分数量
@prop({ default: 0 })
public score: number
@prop({ type: () => [Item], default: [] })
public items: Item[]
public toJson() {
return {
day: this.dateTag,
score: this.score,
items: this.items,
}
}
}
export const DrawRecord = getModelForClass(DrawRecordClass, { existingConnection: DrawRecordClass['db'] })

View File

@ -0,0 +1,38 @@
import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
@dbconn()
@index({ user: 1 }, { unique: false })
@index({ user: 1, type: 1 }, { unique: false })
@index({ user: 1, score: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'ingame_score_record', timestamps: true },
})
class InGameScoreRecordClass extends BaseModule {
@prop({ required: true })
public user: string
@prop()
public score: number
@prop()
public type: string
@prop({ type: mongoose.Schema.Types.Mixed })
public data: any
public toJson() {
return {
user: this.user,
score: this.score,
type: this.type,
//@ts-ignore
time: this.createdAt.getTime(),
}
}
}
export const InGameScoreRecord = getModelForClass(InGameScoreRecordClass, {
existingConnection: InGameScoreRecordClass['db'],
})

24
src/models/InGameStats.ts Normal file
View File

@ -0,0 +1,24 @@
import { dbconn } from 'decorators/dbconn'
import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
import { BaseModule } from './Base'
/**
*
*/
@dbconn()
@index({ user: 1 }, { unique: true })
@modelOptions({
schemaOptions: { collection: 'ingame_stats', timestamps: true },
})
export class InGameStatsClass extends BaseModule {
@prop()
public user: string
// 游戏内抽奖次数
@prop({ default: 0 })
public ticket: number
// 游戏内积分
@prop({ default: 0 })
public score: number
}
export const InGameStats = getModelForClass(InGameStatsClass, { existingConnection: InGameStatsClass['db'] })

View File

@ -0,0 +1,34 @@
import { Severity, getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
/**
* ingame task record
*/
@dbconn()
@index({ user: 1, dateTag: 1 }, { unique: false })
@index({ user: 1, task: 1 }, { unique: false })
@index({ user: 1, task: 1, dateTag: 1 }, { unique: true })
@modelOptions({
schemaOptions: { collection: 'ingame_task_record', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
class InGameTaskRecordClass extends BaseModule {
@prop({ required: true })
public user: string
@prop({ required: true })
public task: string
@prop()
public dateTag: string
// 获得的积分数量
@prop({ default: 0 })
public score: number
// 获得的抽奖次数
@prop({ default: 0 })
public ticket: number
}
export const InGameTaskRecord = getModelForClass(InGameTaskRecordClass, {
existingConnection: InGameTaskRecordClass['db'],
})

View File

@ -0,0 +1,24 @@
import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
@dbconn()
@index({ address: 1, tokenId: 1, dateTag: 1 }, { unique: true })
@index({ address: 1, dateTag: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'nft_claim_record', timestamps: true },
})
class NftClaimRecordClass extends BaseModule {
@prop()
public address: string
@prop()
public tokenId: string
@prop()
public dateTag: string
}
export const NftClaimRecord = getModelForClass(NftClaimRecordClass, {
existingConnection: NftClaimRecordClass['db'],
})

View File

@ -3,7 +3,7 @@ import { dbconn } from 'decorators/dbconn'
import { BaseModule } from '../Base' import { BaseModule } from '../Base'
import { ZERO_ADDRESS } from 'common/Constants' import { ZERO_ADDRESS } from 'common/Constants'
@dbconn('chain') @dbconn('chain2')
@index({ chain: 1, address: 1, tokenId: 1 }, { unique: true }) @index({ chain: 1, address: 1, tokenId: 1 }, { unique: true })
@index({ chain: 1, address: 1, user: 1 }, { unique: false }) @index({ chain: 1, address: 1, user: 1 }, { unique: false })
@modelOptions({ @modelOptions({

View File

@ -4,6 +4,8 @@ import { ActivityInfo, ActivityInfoClass } from 'models/ActivityInfo'
import { ActivityUser, ActivityUserClass } from 'models/ActivityUser' import { ActivityUser, ActivityUserClass } from 'models/ActivityUser'
import { ROLE_ANON } from 'zutils' import { ROLE_ANON } from 'zutils'
import { DocumentType } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose'
import { ReadOnlyCache } from 'common/ReadOnlyCache'
import { ACTIVITY_NAME } from 'common/Constants'
declare module 'fastify' { declare module 'fastify' {
interface FastifyRequest { interface FastifyRequest {
@ -44,12 +46,16 @@ const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastif
return reply.send({ errcode: 10, errmsg: 'need login' }) return reply.send({ errcode: 10, errmsg: 'need login' })
} }
request.user = account request.user = account
if (account.activity) { let activity = new ReadOnlyCache().getData(ACTIVITY_NAME)
let activity = await ActivityInfo.findById(account.activity) if (!activity) {
activity = await ActivityInfo.findById(ACTIVITY_NAME)
if (activity) { if (activity) {
request.activity = activity new ReadOnlyCache().setData(ACTIVITY_NAME, activity)
} }
} }
if (activity) {
request.activity = activity
}
} catch (err) { } catch (err) {
return reply.send({ errcode: 401, errmsg: 'need auth' }) return reply.send({ errcode: 401, errmsg: 'need auth' })
} }

View File

@ -1,5 +1,7 @@
import { LotteryCache } from 'common/LotteryCache' import { ACTIVITY_NAME } from 'common/Constants'
import { ReadOnlyCache } from 'common/ReadOnlyCache'
import logger from 'logger/logger' import logger from 'logger/logger'
import { ActivityInfo } from 'models/ActivityInfo'
import * as schedule from 'node-schedule' import * as schedule from 'node-schedule'
import { singleton } from 'zutils' import { singleton } from 'zutils'
@ -10,14 +12,16 @@ import { singleton } from 'zutils'
export default class CacheSchedule { export default class CacheSchedule {
async updateCache() { async updateCache() {
try { try {
new LotteryCache().flush() const activity = await ActivityInfo.findById(ACTIVITY_NAME)
new ReadOnlyCache().setData(ACTIVITY_NAME, activity)
} catch (err) { } catch (err) {
logger.warn(err) logger.warn(err)
} }
} }
scheduleAll() { scheduleAll() {
schedule.scheduleJob('*/10 * * * * *', async () => { schedule.scheduleJob('*/1 * * * *', async () => {
await this.updateCache() await this.updateCache()
}) })
this.updateCache()
} }
} }

View File

@ -2,6 +2,16 @@ import { STEP_CHEST_RATE, STEP_SCORE_MAX, STEP_SCORE_MIN } from 'common/Constant
import { ActivityChest, ActivityChestClass, ChestStatusEnum } from 'models/ActivityChest' import { ActivityChest, ActivityChestClass, ChestStatusEnum } from 'models/ActivityChest'
import { ZError, ZRedisClient } from 'zutils' import { ZError, ZRedisClient } from 'zutils'
import { DocumentType } from '@typegoose/typegoose' import { DocumentType } from '@typegoose/typegoose'
import { timeoutFetch } from 'zutils/utils/net.util'
export const drawCfgs = require('../../configs/draw_cfg.json')
// 处理draw_cfg.json中的probability, 计算出每个奖励的概率区间
let probability = 0
for (let i = 0; i < drawCfgs.length; i++) {
let cfg = drawCfgs[i]
probability += cfg.probability
cfg.probability = probability
}
const chestCfg = require('../../configs/chest.json') const chestCfg = require('../../configs/chest.json')
const chestBonusItems = require('../../configs/chest_bonus_item.json') const chestBonusItems = require('../../configs/chest_bonus_item.json')
@ -121,3 +131,56 @@ export const generateChestBonus = async (chest: DocumentType<ActivityChestClass>
}) })
return rewards return rewards
} }
export const queryInGameInfo = async (openId: string, channel: string) => {
let url = `${process.env.GAME_SERVER_URL}&channel=${channel}&openId=${openId}`
const key = `gameinfo:${openId}:${channel}`
const cache = await new ZRedisClient().get(key)
if (cache) {
try {
let result = JSON.parse(cache)
return result
} catch (e) {
console.error(e)
}
}
let res = await timeoutFetch(url, {}, 5000)
let data = await res.json()
if (data.errcode) {
throw new ZError(data.errcode, data.errmsg)
}
let result = data.info
new ZRedisClient().pub.set(key, JSON.stringify(result), 'EX', 60, () => {
console.log('cache set success: ', key)
})
return result
}
/**
* drawCfgs中设定的probability,
* type为1或2, redis中spop一个值, , ,
* */
export const drawOnce = async (hasWhite: boolean) => {
let random = Math.random() * probability
let reward
for (let i = 0; i < drawCfgs.length; i++) {
let cfg = drawCfgs[i]
if (random < cfg.probability) {
// deep copy
reward = JSON.parse(JSON.stringify(cfg))
break
}
}
if (!reward) {
return drawOnce(hasWhite)
}
if (reward.type === 1 || reward.type === 2) {
const redisKey = `draw_${reward.type}`
const itemId = await new ZRedisClient().spop(redisKey)
if (!itemId) {
return drawOnce(hasWhite)
}
reward.id = itemId
}
return reward
}

View File

@ -0,0 +1,71 @@
import { INGAME_SCORE_KEY, RANK_SCORE_SCALE } from 'common/Constants'
import { InGameScoreRecord } from 'models/InGameScoreRecord'
import { InGameStats } from 'models/InGameStats'
import { ZRedisClient } from 'zutils'
const rankLevels = require('../../configs/uaw_rank_level.json')
/**
*
* @param param0
* user: 用户id
* score: 分数
* scoreType: 分数类型
* scoreParams: 额外的参数
*/
export const updateRankScore = async ({
user,
score,
scoreType,
scoreParams,
}: {
user: string
score: number
scoreType: string
scoreParams: any
rebateLevel?: number
}) => {
let record = new InGameScoreRecord({
user: user,
score,
type: scoreType,
data: scoreParams,
})
await record.save()
await InGameStats.updateOne({ user }, { $inc: { score } })
const key = INGAME_SCORE_KEY
await updateRank(key, score, user)
// 给邀请人返利
// if (rebateLevel > 0) {
// let userMod = await ActivityUser.findById(user)
// if (userMod?.inviteUser) {
// const score1 = score * INVITE_REBATE
// let scoreParams1 = Object.assign({ fromUser: user }, scoreParams)
// const keyInvite = `${activity}:invite`
// await updateRankInvite(keyInvite, score1, `${userMod.inviteUser}_${user}`)
// await updateRankScore({
// user: userMod.inviteUser,
// score: score1,
// activity,
// scoreType: SCORE_INVITE_REBATE,
// scoreParams: scoreParams1,
// rebateLevel: rebateLevel - 1,
// })
// }
// }
}
/**
*
* @param key
* @param score
* @param member
*/
export const updateRank = async (key: string, score: number, member: string) => {
score = parseInt(score * RANK_SCORE_SCALE + '')
await new ZRedisClient().zincrby(key, score, member)
}
export const rankLevel = (rank: number | string) => {
rank = parseInt(rank + '')
const data = rankLevels.find(o => rank >= o.rankMin && rank <= o.rankMax)
return data ? data.level : '-'
}

View File

@ -0,0 +1,10 @@
import { ITask } from './base/ITask'
export default class GameBattle extends ITask {
static desc = 'game battle'
static key = 'battleTimes'
static show: boolean = true
async execute(data: any) {
return true
}
}

View File

@ -0,0 +1,13 @@
import { ITask } from './base/ITask'
import { TaskStatusEnum } from 'models/ActivityUser'
import { ZError } from 'zutils'
import { TaskCfg } from 'models/ActivityInfo'
export default class GameCoin extends ITask {
static desc = 'game coin'
static key = 'getGoldVal'
static show: boolean = true
async execute(data: any) {
return true
}
}

View File

@ -0,0 +1,13 @@
import { ITask } from './base/ITask'
import { TaskStatusEnum } from 'models/ActivityUser'
import { ZError } from 'zutils'
import { TaskCfg } from 'models/ActivityInfo'
export default class GameKill extends ITask {
static desc = 'game kill'
static key = 'kills'
static show: boolean = true
async execute(data: any) {
return true
}
}

View File

@ -0,0 +1,10 @@
import { ITask } from './base/ITask'
export default class GameLogin extends ITask {
static desc = 'game login'
static key = 'loginVal'
static show: boolean = true
async execute(data: any) {
return true
}
}

10
src/taskingame/GameWin.ts Normal file
View File

@ -0,0 +1,10 @@
import { ITask } from './base/ITask'
export default class GameWin extends ITask {
static desc = 'game win'
static key = 'winTimes'
static show: boolean = true
async execute(data: any) {
return true
}
}

View File

@ -0,0 +1,21 @@
import { ZError } from 'zutils'
import { ITask } from './base/ITask'
export default class GoogleConnect extends ITask {
static desc = 'connect google account'
static show: boolean = true
async execute(data: any) {
return true
}
public async claimReward(task: any) {
if (!this.user.googleId) {
throw new ZError(100, 'google account not binded')
}
return { score: 0, ticket: 0 }
}
public async check(cfg: any, gameData: any) {
return !!this.user.googleId
}
}

View File

@ -0,0 +1,86 @@
import { NftHolder } from 'models/chain/NftHolder'
import { ITask } from './base/ITask'
import { formatDate } from 'utils/utcdate.util'
import { NftClaimRecord } from 'models/NftClaimRecord'
import { ZError } from 'zutils'
const sourceList = require('../../configs/nft_202403.json')
const commonSet = new Set()
const legendarySet = new Set()
const rareSet = new Set()
for (let o of sourceList) {
if (o.rarity === 'Common') {
commonSet.add(o.id)
} else if (o.rarity === 'Legendary') {
legendarySet.add(o.id)
} else if (o.rarity === 'Rare') {
rareSet.add(o.id)
}
}
export const tokenRarity = (tokenId: string | number) => {
if (commonSet.has(parseInt(tokenId + ''))) {
return 'Common'
} else if (legendarySet.has(parseInt(tokenId + ''))) {
return 'Legendary'
} else if (rareSet.has(parseInt(tokenId + ''))) {
return 'Rare'
}
}
const rarityList = require('../../configs/nft_rarity.json')
export const rarityMap = new Map()
for (let o of rarityList) {
rarityMap.set(o.rarity, o.ticket)
}
export default class NftHolderCheck extends ITask {
static desc = 'nft holder'
static show: boolean = true
async execute(data: any) {
return true
}
public async claimReward(cfg: any) {
const { chain, address } = cfg.cfg
let score = 0
let ticket = 0
const dateTag = formatDate(new Date())
let historys = await NftClaimRecord.find({ address, dateTag })
let historySet = new Set()
for (let history of historys) {
historySet.add(history.tokenId)
}
let records = await NftHolder.find({ chain, address, user: this.user.address.toLowerCase() })
// remove record which tokenId in historySet first
records = records.filter(o => !historySet.has(o.tokenId))
if (records.length === 0) {
throw new ZError(20, 'not NFT holder')
}
for (let record of records) {
if (commonSet.has(parseInt(record.tokenId))) {
ticket += rarityMap.get('Common')
} else if (legendarySet.has(parseInt(record.tokenId))) {
ticket += rarityMap.get('Legendary')
} else if (rareSet.has(parseInt(record.tokenId))) {
ticket += rarityMap.get('Rare')
}
let claimRecord = new NftClaimRecord({
address,
tokenId: record.tokenId,
dateTag,
})
await claimRecord.save()
}
return { score, ticket }
}
public async check(cfg: any, gameData: any) {
const { chain, address } = cfg.cfg
const dateTag = formatDate(new Date())
let historys = await NftClaimRecord.find({ address, dateTag })
let historySet = new Set()
for (let history of historys) {
historySet.add(history.tokenId)
}
let records = await NftHolder.find({ chain, address, user: this.user.address.toLowerCase() })
return records.length > 0
}
}

View File

@ -0,0 +1,90 @@
import { ActivityInfoClass, TaskCfg } from 'models/ActivityInfo'
import { ActivityItem } from 'models/ActivityItem'
import { ActivityUserClass } from 'models/ActivityUser'
import { DocumentType } from '@typegoose/typegoose'
import { queryInGameInfo } from 'services/game.svr'
import { ZError } from 'zutils'
import { updateRankScore } from 'services/ingame.rank.svr'
import { formatDate } from 'utils/utcdate.util'
import { InGameTaskRecord } from 'models/InGameTaskRecord'
import { InGameStats } from 'models/InGameStats'
export abstract class ITask {
static desc: string
static key: string
static show: boolean = true
static auto: boolean = false
user: DocumentType<ActivityUserClass>
activity: DocumentType<ActivityInfoClass>
constructor({
user,
activity,
}: {
user: DocumentType<ActivityUserClass>
activity: DocumentType<ActivityInfoClass>
}) {
// do nothing
this.user = user
this.activity = activity
}
abstract execute(data: any): Promise<boolean>
public async claimReward(cfg: any) {
const user = this.user
let gameData = await queryInGameInfo(user.googleId, '0')
if (!(await this.check(cfg, gameData))) {
throw new ZError(50, 'not match condition')
}
const dateTag = formatDate(new Date())
let score = cfg.score
if (score > 0) {
await updateRankScore({
user: user.id,
score: score,
scoreType: 'ingame_task',
scoreParams: {
date: dateTag,
taskId: cfg.id,
boost: user.boost,
},
})
}
let ticket = 0
if (cfg.ticket > 0 && Date.now() >= this.activity.drawTime) {
let canGet = false
if (cfg.tickettasks) {
let records = await InGameTaskRecord.find({ user: user.id, dateTag, task: { $in: cfg.tickettasks } })
if (records.length === cfg.tickettasks.length) {
canGet = true
}
} else {
canGet = true
}
if (canGet) {
ticket = cfg.ticket
await InGameStats.updateOne({ user: user.id }, { $inc: { ticket } })
}
}
return { score, ticket }
}
public async check(cfg: any, gameData: any) {
const subCfg = cfg.cfg
const val = parseInt(gameData[subCfg.key] || '0')
return val >= subCfg.amount
}
public async claimItem(cfg: TaskCfg) {
if (cfg.params.items) {
for (let key in cfg.params.items) {
let amount = cfg.params.items[key]
await ActivityItem.insertOrUpdate(
{ user: this.user.id, activity: this.user.activity, item: key },
{ $inc: { amount }, last: Date.now() },
)
}
}
}
}