mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2025-12-13 23:56:25 +08:00
77
README.md
77
README.md
@@ -91,73 +91,24 @@ It is just so interesting. What it can do is under exploration.
|
||||
|
||||
### Version 2.12.0
|
||||
|
||||
> v2.11.3.1 ~ v2.11.3.20 for Arcaea 5.2.0 ~ 5.10.4
|
||||
> v2.12.0.1 ~ v2.12.0.10 for Arcaea 5.10.6 ~ 6.7.1
|
||||
>
|
||||
> Here are not some bug fixes.
|
||||
>
|
||||
> 注意:Link Play 2.0 无法兼容旧版本客户端。 Note: Link Play 2.0 is not compatible with older client versions.
|
||||
> Here are not all bug fixes and features.
|
||||
|
||||
- 适用于 Arcaea 5.10.4 版本
|
||||
For Arcaea 5.10.4
|
||||
- 适用于 Arcaea 6.7.1 版本
|
||||
For Arcaea 6.7.1
|
||||
- 添加一些新搭档和搭档的觉醒形态,并支持他们的技能
|
||||
Add some new partners, uncap some others, and add support for their skills.
|
||||
- 支持 Link Play 2.0 的几乎所有功能
|
||||
Add almost whole support for Link Play 2.0.
|
||||
- 支持新谱面难度 ETR
|
||||
Adapt to the new difficulty ETR.
|
||||
- 支持内容捆绑包(热更新),包含两种更新模式
|
||||
Add support for content bundles (hot update), including two update modes.
|
||||
- 支持新手任务系统
|
||||
Add support for missions.
|
||||
- 更新 Recent 30 机制,修改其表结构
|
||||
Update Recent 30 mechanism. Alter Recent 30 table structure.
|
||||
- PTT 机制更新:添加了推分保护
|
||||
PTT mechanism: Change first play protection to new best protection.
|
||||
- 调整世界排名机制使其更接近于官服
|
||||
Adjust world rank mechanism to be closer to the official one.
|
||||
- 重构世界模式,并调整了一些搭档的技能效果和进度计算逻辑
|
||||
Code refactor for World Mode, and adjust some skills and the logic of progress calculation.
|
||||
- 支持世界模式的陷落梯子
|
||||
Add support for Breached World Map.
|
||||
- 添加了一个陷落梯子例子(#148)
|
||||
Add an example breached map. (#148)
|
||||
- 变更残片购买体力的恢复时间为 23 小时
|
||||
Change the recover time of using fragments buying stamina to 23 hours.
|
||||
- 支持设置多个可使用的和旧的游戏 API 前缀,其中旧的前缀会通知用户更新客户端
|
||||
Add some endpoints for old API prefixes to notify users to update the client; add support for multiple game prefixes.
|
||||
- 支持用户自销毁账号(默认不开启)
|
||||
Add support for users destroy their own accounts. (default unable)
|
||||
- 添加对“单曲兑换券”的不完整支持
|
||||
Incomplete support for "pick_ticket".
|
||||
- 世界模式地图文件夹中可以包含子文件夹了
|
||||
Make the world maps' folder can have sub folders.
|
||||
- 支持后台和 API 刷新 Recent 30 的定数评分
|
||||
Add support for refreshing ratings of Recent 30 via API and webpage.
|
||||
- 添加对 IP 及设备的用户注册频率限制
|
||||
Add the IP and the device rate limiters for user register.
|
||||
- 修复当用户再次通过已经通过的段位时无法正常上传分数的问题(by Guzi422)
|
||||
Fix the bug that the player cannot upload the score when completing a course again. (by Guzi422)
|
||||
- 修复段位模式最高分在用户未完整完成挑战时不更新的逻辑问题
|
||||
Fix a logical bug that the course's high score will not update if the user does not complete the whole course challenge.
|
||||
- 修复 Link Play 相关 API 接口报错的问题
|
||||
Fix a bug that API for Link Play cannot work.
|
||||
- 修复依赖问题:cryptography >= 35.0.0
|
||||
Fix requirements: cryptography >= 35.0.0
|
||||
- 修复 `songlist` 解析问题(#156)
|
||||
Fix a `songlist` parser problem. (#156)
|
||||
- 修复技能 skill_amane 在世界地图台阶类型为空时报错的问题
|
||||
Fix a bug that "skill_amane" may arise error when the step type of world map is null.
|
||||
- 支持自动添加搭档“光 & 对立 (Reunion)”和“光 (Fatalis)”,以尝试解决最终章的解锁问题(#110 #164)
|
||||
Add support for automatically adding partner "Hikari & Tairitsu (Reunion)" and "Hikari (Fatalis)", to try to unlock Finale stories correctly. (#110 #164)
|
||||
- 修复 `songlist` 文件存在时视频文件无法下载的问题(#177)
|
||||
Fix a bug that the video files cannot be downloaded when
|
||||
the `songlist` file exists. (#177)
|
||||
- 修复 Link Play 中玩家全部返回房间后上一首曲子成绩消失的问题
|
||||
Fix a bug that the last song's scores will disappear when all players return to room in Link Play.
|
||||
- 工具 `update_song.py` 支持 ETR 难度
|
||||
Add support for ETR difficulties in the `update_song.py` tool.
|
||||
- 添加发送错误信息的小工具测试服务端
|
||||
Add a small tool test server to send error message.
|
||||
- 支持“光 (Fatalis)”的由世界模式总台阶数决定的动态搭档数值
|
||||
Add support for dynamic values of "Hikari (Fatalis)", which is depended by world mode total steps.
|
||||
- 支持新版本客户端的账户注册和登录接口
|
||||
Adapt to new account registration and login endpoints of the new version client.
|
||||
- 修复 Link Play 房间中玩家历史分数异常的问题
|
||||
Fix a bug that if players do not start the first play in one room, their scores will be wrong.
|
||||
- 修复因为歌曲 idx 数值过大导致服务端错误,致使 Link Play 模块无法工作的问题
|
||||
Fix a bug that if song's idx is too big, the server will catch the error, which makes Link Play cannot work.
|
||||
- 修复谱面难度 ETR 相关问题
|
||||
Fix some bugs about the ETR difficulty.
|
||||
|
||||
## 运行环境与依赖 Running environment and requirements
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||
from .item import CollectionItemMixin, ItemCore
|
||||
from .sql import UserKVTable
|
||||
|
||||
|
||||
class Level:
|
||||
@@ -55,6 +56,7 @@ class Skill:
|
||||
class CharacterValue:
|
||||
def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
|
||||
self.set_parameter(start, mid, end)
|
||||
self.addition: float = 0
|
||||
|
||||
@staticmethod
|
||||
def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
|
||||
@@ -87,9 +89,9 @@ class CharacterValue:
|
||||
|
||||
def get_value(self, level: Level):
|
||||
if level.min_level <= level.level <= level.mid_level:
|
||||
return self._calc_char_value_20_math(level.level, self.start, self.mid)
|
||||
return self._calc_char_value_20_math(level.level, self.start, self.mid) + self.addition
|
||||
if level.mid_level < level.level <= level.max_level:
|
||||
return self._calc_char_value_30(level.level, self.mid, self.end)
|
||||
return self._calc_char_value_30(level.level, self.mid, self.end) + self.addition
|
||||
return 0
|
||||
|
||||
|
||||
@@ -231,6 +233,8 @@ class UserCharacter(Character):
|
||||
|
||||
self.skill_flag: bool = None
|
||||
|
||||
self.fatalis_is_limited: bool = False
|
||||
|
||||
@property
|
||||
def skill_id_displayed(self) -> str:
|
||||
'''对外显示的技能id'''
|
||||
@@ -295,6 +299,22 @@ class UserCharacter(Character):
|
||||
if self.character_id in (21, 46):
|
||||
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
|
||||
if self.character_id == 55:
|
||||
# fatalis 提升数值
|
||||
# prog & overdrive += 世界模式中完成的所有非无限地图的台阶数之和 / 30
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
addition = Constant.FATALIS_MAX_VALUE
|
||||
self.fatalis_is_limited = True
|
||||
else:
|
||||
kvd = UserKVTable(self.c, self.user.user_id, 'world')
|
||||
steps = kvd['total_step_count'] or 0
|
||||
addition = steps / 30
|
||||
if addition >= Constant.FATALIS_MAX_VALUE:
|
||||
addition = Constant.FATALIS_MAX_VALUE
|
||||
self.fatalis_is_limited = True
|
||||
self.prog.addition = addition
|
||||
self.overdrive.addition = addition
|
||||
|
||||
self.select_character_core()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -321,7 +341,7 @@ class UserCharacter(Character):
|
||||
if self.voice:
|
||||
r['voice'] = self.voice
|
||||
if self.character_id == 55:
|
||||
r['fatalis_is_limited'] = False # emmmmmmm
|
||||
r['fatalis_is_limited'] = self.fatalis_is_limited
|
||||
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
|
||||
r['base_character_id'] = 1
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class Config:
|
||||
|
||||
SONG_FILE_HASH_PRE_CALCULATE = True
|
||||
|
||||
GAME_API_PREFIX = '/autumnequinox/33' # str | list[str]
|
||||
GAME_API_PREFIX = ['/coldwind/35', '/'] # str | list[str]
|
||||
OLD_GAME_API_PREFIX = [] # str | list[str]
|
||||
|
||||
ALLOW_APPVERSION = [] # list[str]
|
||||
@@ -86,6 +86,9 @@ class Config:
|
||||
BEST30_WEIGHT = 1 / 40
|
||||
RECENT10_WEIGHT = 1 / 40
|
||||
|
||||
INVASION_START_WEIGHT = 0.1
|
||||
INVASION_HARD_WEIGHT = 0.1
|
||||
|
||||
MAX_FRIEND_COUNT = 50
|
||||
|
||||
WORLD_MAP_FOLDER_PATH = './database/map/'
|
||||
@@ -103,9 +106,7 @@ class Config:
|
||||
GAME_REGISTER_IP_RATE_LIMIT = '10/1 day'
|
||||
GAME_REGISTER_DEVICE_RATE_LIMIT = '3/1 day'
|
||||
|
||||
|
||||
NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000
|
||||
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .config_manager import Config
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.11.3.20'
|
||||
ARCAEA_DATABASE_VERSION = 'v2.11.3.19'
|
||||
ARCAEA_SERVER_VERSION = 'v2.12.1'
|
||||
ARCAEA_DATABASE_VERSION = 'v2.12.1'
|
||||
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class Constant:
|
||||
|
||||
MAX_STAMINA = 12
|
||||
|
||||
# INSIGHT_STATES = [x for x in range(7)]
|
||||
INSIGHT_TOGGLE_STATES = [3, 4, 5, 6]
|
||||
# DEFAULT_INSIGHT_STATE = Config.DEFAULT_INSIGHT_STATE
|
||||
|
||||
STAMINA_RECOVER_TICK = 1800000
|
||||
FRAGSTAM_RECOVER_TICK = 23 * 3600 * 1000
|
||||
|
||||
@@ -32,6 +36,7 @@ class Constant:
|
||||
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
|
||||
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
|
||||
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
|
||||
FATALIS_MAX_VALUE = 100
|
||||
|
||||
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
||||
|
||||
@@ -40,6 +45,8 @@ class Constant:
|
||||
|
||||
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
||||
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
||||
INVASION_START_WEIGHT = Config.INVASION_START_WEIGHT
|
||||
INVASION_HARD_WEIGHT = Config.INVASION_HARD_WEIGHT
|
||||
|
||||
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
|
||||
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
|
||||
@@ -66,7 +73,6 @@ class Constant:
|
||||
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
|
||||
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||
|
||||
|
||||
LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4 # Units: seconds
|
||||
LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000]
|
||||
LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1]
|
||||
|
||||
@@ -23,6 +23,8 @@ def get_song_unlock(client_song_map: 'dict[str, list]') -> bytes:
|
||||
if not v[i]:
|
||||
continue
|
||||
index = int(k) * 5 + i
|
||||
if index >= Constant.LINKPLAY_UNLOCK_LENGTH * 8:
|
||||
continue
|
||||
user_song_unlock[index // 8] |= 1 << (index % 8)
|
||||
|
||||
return bytes(user_song_unlock)
|
||||
|
||||
@@ -171,9 +171,9 @@ class SaveUpdateScore(BaseOperation):
|
||||
song_id = list(set(song_id_1 + song_id_2))
|
||||
|
||||
c.execute(
|
||||
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
|
||||
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
|
||||
x = c.fetchall()
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4], i[5]]
|
||||
for i in x} # chart const * 10
|
||||
|
||||
new_scores = []
|
||||
@@ -204,8 +204,8 @@ class SaveUpdateScore(BaseOperation):
|
||||
def _all_update(self):
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart''')
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4], i[5]]
|
||||
for i in c.fetchall()} # chart const * 10
|
||||
c.execute('''select user_id from user_save''')
|
||||
for y in c.fetchall():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from base64 import b64encode
|
||||
from os import urandom
|
||||
from random import choices
|
||||
from time import time
|
||||
|
||||
from .bgtask import BGTask, logdb_execute
|
||||
@@ -244,10 +245,15 @@ class UserPlay(UserScore):
|
||||
self.course_play: 'CoursePlay' = None
|
||||
|
||||
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.hp_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.fever_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.skill_cytusii_flag: str = None
|
||||
self.skill_chinatsu_flag: str = None
|
||||
self.highest_health: int = None
|
||||
self.lowest_health: int = None
|
||||
|
||||
self.invasion_flag: int = None # 1: invasion_start, 2: invasion_hard
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
# 不能super
|
||||
if self.is_world_mode is None or self.course_play_state is None:
|
||||
@@ -288,6 +294,13 @@ class UserPlay(UserScore):
|
||||
return False
|
||||
x = x + str(self.combo_interval_bonus)
|
||||
|
||||
if self.hp_interval_bonus is not None and self.hp_interval_bonus < 0:
|
||||
return False
|
||||
|
||||
if self.fever_bonus is not None and (self.fever_bonus < 0 or self.fever_bonus > self.perfect_count * 5):
|
||||
# fever 等级最高为 5
|
||||
return False
|
||||
|
||||
y = f'{self.user.user_id}{self.song_hash}'
|
||||
checksum = md5(x+md5(y))
|
||||
|
||||
@@ -326,16 +339,27 @@ class UserPlay(UserScore):
|
||||
self.prog_boost_multiply = int(x[10])
|
||||
self.beyond_boost_gauge_usage = int(x[11])
|
||||
self.skill_cytusii_flag = x[12]
|
||||
self.skill_chinatsu_flag = x[13]
|
||||
self.invasion_flag = x[14]
|
||||
self.is_world_mode = True
|
||||
self.course_play_state = -1
|
||||
|
||||
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0, skill_cytusii_flag: str = None) -> None:
|
||||
def set_play_state_for_world(
|
||||
self,
|
||||
stamina_multiply: int = 1,
|
||||
fragment_multiply: int = 100,
|
||||
prog_boost_multiply: int = 0,
|
||||
beyond_boost_gauge_usage: int = 0,
|
||||
skill_cytusii_flag: str = None,
|
||||
skill_chinatsu_flag: str = None
|
||||
) -> None:
|
||||
self.song_token = b64encode(urandom(64)).decode()
|
||||
self.stamina_multiply = int(stamina_multiply)
|
||||
self.fragment_multiply = int(fragment_multiply)
|
||||
self.prog_boost_multiply = int(prog_boost_multiply)
|
||||
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
|
||||
self.skill_cytusii_flag = skill_cytusii_flag
|
||||
self.skill_chinatsu_flag = skill_chinatsu_flag
|
||||
if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
|
||||
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
|
||||
'a': self.user.user_id})
|
||||
@@ -346,10 +370,6 @@ class UserPlay(UserScore):
|
||||
# 注意:偷懒了,没判断是否是beyond图
|
||||
self.beyond_boost_gauge_usage = 0
|
||||
|
||||
self.clear_play_state()
|
||||
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h)''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 't': self.song_token})
|
||||
|
||||
self.user.select_user_about_current_map()
|
||||
self.user.current_map.select_map_info()
|
||||
|
||||
@@ -357,17 +377,26 @@ class UserPlay(UserScore):
|
||||
if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
|
||||
raise StaminaNotEnough('Stamina is not enough.')
|
||||
|
||||
fatalis_stamina_multiply = 1
|
||||
self.user.select_user_about_character()
|
||||
if not self.user.is_skill_sealed:
|
||||
self.user.character.select_character_info()
|
||||
if self.user.character.skill_id_displayed == 'skill_fatalis':
|
||||
# 特殊判断hikari fatalis的双倍体力消耗
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
||||
self.stamina_multiply * 2
|
||||
self.user.stamina.update()
|
||||
return None
|
||||
# invasion 扔骰子
|
||||
_flag = choices([0, 1, 2], [
|
||||
max(1 - Constant.INVASION_START_WEIGHT - Constant.INVASION_HARD_WEIGHT, 0), Constant.INVASION_START_WEIGHT, Constant.INVASION_HARD_WEIGHT])[0]
|
||||
if self.user.is_insight_enabled and _flag != 0:
|
||||
self.invasion_flag = _flag
|
||||
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * self.stamina_multiply
|
||||
elif self.user.character.skill_id_displayed == 'skill_fatalis':
|
||||
# 特殊判断hikari fatalis的双倍体力消耗
|
||||
fatalis_stamina_multiply = 2
|
||||
|
||||
self.clear_play_state()
|
||||
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h,:i,:j)''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 'i': self.skill_chinatsu_flag, 'j': self.invasion_flag, 't': self.song_token})
|
||||
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
||||
self.stamina_multiply * fatalis_stamina_multiply
|
||||
self.user.stamina.update()
|
||||
|
||||
def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
|
||||
@@ -380,8 +409,8 @@ class UserPlay(UserScore):
|
||||
self.course_play.score = 0
|
||||
self.course_play.clear_type = 3 # 设置为PM,即最大值
|
||||
|
||||
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0,"")''', (self.song_token, self.user.user_id, self.song.song_id,
|
||||
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
||||
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0,"","",0)''', (self.song_token, self.user.user_id, self.song.song_id,
|
||||
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
||||
self.user.select_user_about_stamina()
|
||||
if use_course_skip_purchase:
|
||||
x = ItemCore(self.c)
|
||||
|
||||
@@ -525,3 +525,35 @@ class MemoryDatabase:
|
||||
@register
|
||||
def atexit():
|
||||
MemoryDatabase.conn.close()
|
||||
|
||||
|
||||
class UserKVTable:
|
||||
'''用户键值对表'''
|
||||
|
||||
def __init__(self, c=None, user_id: int = None, class_name: str = None) -> None:
|
||||
self.c = c
|
||||
self.user_id = user_id
|
||||
self.class_name = class_name
|
||||
|
||||
def get(self, key: str, idx: int = 0):
|
||||
'''获取键值对'''
|
||||
x = self.c.execute(
|
||||
'''select value from user_kvdata where user_id = ? and class = ? and key = ? and idx = ?''', (self.user_id, self.class_name, key, idx)).fetchone()
|
||||
return x[0] if x else None
|
||||
|
||||
def set(self, key: str, value, idx: int = 0) -> None:
|
||||
'''设置键值对'''
|
||||
self.c.execute('''insert or replace into user_kvdata values(?,?,?,?,?)''',
|
||||
(self.user_id, self.class_name, key, idx, value))
|
||||
|
||||
def __getitem__(self, args):
|
||||
if isinstance(args, tuple):
|
||||
return self.get(*args)
|
||||
else:
|
||||
return self.get(args)
|
||||
|
||||
def __setitem__(self, args, value):
|
||||
if isinstance(args, tuple):
|
||||
self.set(args[0], value, args[1])
|
||||
else:
|
||||
self.set(args, value)
|
||||
|
||||
@@ -13,8 +13,8 @@ from .item import UserItemList
|
||||
from .limiter import ArcLimiter
|
||||
from .mission import UserMissionList
|
||||
from .score import Score
|
||||
from .sql import Query, Sql
|
||||
from .world import Map, UserMap, UserStamina
|
||||
from .sql import Query, Sql, UserKVTable
|
||||
from .world import Map, MapParser, UserMap, UserStamina
|
||||
|
||||
|
||||
def code_get_id(c, user_code: str) -> int:
|
||||
@@ -326,6 +326,10 @@ class UserInfo(User):
|
||||
self.current_map: 'Map' = None
|
||||
self.stamina: 'UserStamina' = None
|
||||
|
||||
self.insight_state: int = None
|
||||
|
||||
self.custom_banner = None
|
||||
|
||||
self.__cores: list = None
|
||||
self.__packs: list = None
|
||||
self.__singles: list = None
|
||||
@@ -336,6 +340,12 @@ class UserInfo(User):
|
||||
self.curr_available_maps: list = None
|
||||
self.__course_banners: list = None
|
||||
|
||||
@property
|
||||
def is_insight_enabled(self) -> bool:
|
||||
if self.insight_state is None:
|
||||
self.select_user_one_column('insight_state', 4, int)
|
||||
return self.insight_state == 3 or self.insight_state == 5
|
||||
|
||||
@property
|
||||
def cores(self) -> list:
|
||||
if self.__cores is None:
|
||||
@@ -545,8 +555,17 @@ class UserInfo(User):
|
||||
'pick_ticket': self.pick_ticket,
|
||||
|
||||
# 'custom_banner': 'online_banner_2024_06',
|
||||
'custom_banner': self.custom_banner,
|
||||
# 'subscription_multiplier': 114,
|
||||
# 'memory_boost_ticket': 5,
|
||||
'insight_state': self.insight_state, # 0~2 不可选,3 技能激活,4 未激活,5 激活可选,6 未激活可选
|
||||
|
||||
# 'enabled_features': [
|
||||
# {
|
||||
# "metadata": ["USA"],
|
||||
# "feature": "paymentlink"
|
||||
# }
|
||||
# ],
|
||||
}
|
||||
|
||||
def from_list(self, x: list) -> 'UserInfo':
|
||||
@@ -591,6 +610,10 @@ class UserInfo(User):
|
||||
|
||||
self.mp_notification_enabled = x[37] == 1
|
||||
|
||||
self.insight_state = x[38]
|
||||
|
||||
self.custom_banner = x[39] if x[39] is not None else ''
|
||||
|
||||
return self
|
||||
|
||||
def select_user(self) -> None:
|
||||
@@ -719,6 +742,35 @@ class UserInfo(User):
|
||||
'''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id))
|
||||
self.world_rank_score = x[0]
|
||||
|
||||
def update_user_world_complete_info(self) -> None:
|
||||
'''
|
||||
更新用户的世界模式完成信息,包括两个部分
|
||||
|
||||
1. 每个章节的完成地图数量,为了 salt 技能
|
||||
2. 全世界模式完成台阶数之和,为了 fatalis 技能
|
||||
'''
|
||||
kvd = UserKVTable(self.c, self.user_id, 'world')
|
||||
|
||||
for chapter_id, map_ids in MapParser.chapter_info_without_repeatable.items():
|
||||
self.c.execute(
|
||||
f'''select map_id, curr_position from user_world where user_id = ? and map_id in ({','.join(['?']*len(map_ids))})''',
|
||||
(self.user_id, *map_ids)
|
||||
)
|
||||
x = self.c.fetchall()
|
||||
n = 0
|
||||
for map_id, curr_position in x:
|
||||
step_count = MapParser.world_info[map_id]['step_count']
|
||||
if curr_position == step_count - 1:
|
||||
n += 1
|
||||
kvd['chapter_complete_count', chapter_id] = n
|
||||
|
||||
self.c.execute(
|
||||
'''select sum(curr_position) + count(*) from user_world where user_id = ?''', (self.user_id,)
|
||||
)
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
kvd['total_step_count'] = x[0] or 0
|
||||
|
||||
def select_user_one_column(self, column_name: str, default_value=None, data_type=None) -> None:
|
||||
'''
|
||||
查询user表的某个属性
|
||||
@@ -795,6 +847,19 @@ class UserOnline(UserInfo):
|
||||
self.c.execute('''update user set favorite_character = :a where user_id = :b''',
|
||||
{'a': self.favorite_character.character_id, 'b': self.user_id})
|
||||
|
||||
def toggle_invasion(self) -> None:
|
||||
self.c.execute(
|
||||
'''select insight_state from user where user_id = ?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
self.insight_state = x[0]
|
||||
rq = Constant.INSIGHT_TOGGLE_STATES
|
||||
self.insight_state = rq[(rq.index(self.insight_state) + 1) % len(rq)]
|
||||
|
||||
self.c.execute(
|
||||
'''update user set insight_state = ? where user_id = ?''', (self.insight_state, self.user_id))
|
||||
|
||||
|
||||
class UserChanger(UserInfo, UserRegister):
|
||||
|
||||
|
||||
@@ -4,16 +4,23 @@ from json import load
|
||||
from random import randint
|
||||
from time import time
|
||||
|
||||
from .character import Character
|
||||
from .character import Character, UserCharacter
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import InputError, MapLocked, NoData
|
||||
from .item import ItemFactory
|
||||
from .sql import UserKVTable
|
||||
|
||||
|
||||
class MapParser:
|
||||
|
||||
map_id_path: 'dict[str, str]' = {}
|
||||
|
||||
world_info: 'dict[str, dict]' = {} # 简要记录地图信息
|
||||
chapter_info: 'dict[int, list[str]]' = {} # 章节包含的地图
|
||||
# 章节包含的地图(不包含可重复地图)
|
||||
chapter_info_without_repeatable: 'dict[int, list[str]]' = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
if not self.map_id_path:
|
||||
self.parse()
|
||||
@@ -25,10 +32,31 @@ class MapParser:
|
||||
continue
|
||||
|
||||
path = os.path.join(root, file)
|
||||
self.map_id_path[file[:-5]] = path
|
||||
map_id = file[:-5]
|
||||
self.map_id_path[map_id] = path
|
||||
|
||||
map_data = self.get_world_info(map_id)
|
||||
chapter = map_data.get('chapter', None)
|
||||
if chapter is None:
|
||||
continue
|
||||
self.chapter_info.setdefault(chapter, []).append(map_id)
|
||||
is_repeatable = map_data.get('is_repeatable', False)
|
||||
if not is_repeatable:
|
||||
self.chapter_info_without_repeatable.setdefault(
|
||||
chapter, []).append(map_id)
|
||||
self.world_info[map_id] = {
|
||||
'chapter': chapter,
|
||||
'is_repeatable': is_repeatable,
|
||||
'is_beyond': map_data.get('is_beyond', False),
|
||||
'is_legacy': map_data.get('is_legacy', False),
|
||||
'step_count': len(map_data.get('steps', [])),
|
||||
}
|
||||
|
||||
def re_init(self) -> None:
|
||||
self.map_id_path.clear()
|
||||
self.world_info.clear()
|
||||
self.chapter_info.clear()
|
||||
self.chapter_info_without_repeatable.clear()
|
||||
self.get_world_info.cache_clear()
|
||||
self.parse()
|
||||
|
||||
@@ -133,6 +161,7 @@ class Map:
|
||||
self.chain_info: dict = None
|
||||
|
||||
# self.requires: list[dict] = None
|
||||
self.requires_any: 'list[dict]' = None
|
||||
|
||||
self.disable_over: bool = None
|
||||
self.new_law: str = None
|
||||
@@ -188,6 +217,8 @@ class Map:
|
||||
r['disable_over'] = self.disable_over
|
||||
if self.new_law is not None and self.new_law != '':
|
||||
r['new_law'] = self.new_law
|
||||
if self.requires_any:
|
||||
r['requires_any'] = self.requires_any
|
||||
return r
|
||||
|
||||
def from_dict(self, raw_dict: dict) -> 'Map':
|
||||
@@ -216,6 +247,7 @@ class Map:
|
||||
|
||||
self.disable_over = raw_dict.get('disable_over')
|
||||
self.new_law = raw_dict.get('new_law')
|
||||
self.requires_any = raw_dict.get('requires_any')
|
||||
return self
|
||||
|
||||
def select_map_info(self):
|
||||
@@ -467,6 +499,14 @@ class UserStamina(Stamina):
|
||||
|
||||
|
||||
class WorldSkillMixin:
|
||||
'''
|
||||
不可实例化
|
||||
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.user_play = user_play
|
||||
'''
|
||||
|
||||
def before_calculate(self) -> None:
|
||||
factory_dict = {
|
||||
'skill_vita': self._skill_vita,
|
||||
@@ -474,7 +514,11 @@ class WorldSkillMixin:
|
||||
'skill_ilith_ivy': self._skill_ilith_ivy,
|
||||
'ilith_awakened_skill': self._ilith_awakened_skill,
|
||||
'skill_hikari_vanessa': self._skill_hikari_vanessa,
|
||||
'skill_mithra': self._skill_mithra
|
||||
'skill_mithra': self._skill_mithra,
|
||||
'skill_chinatsu': self._skill_chinatsu,
|
||||
'skill_salt': self._skill_salt,
|
||||
'skill_hikari_selene': self._skill_hikari_selene,
|
||||
'skill_nami_sui': self._skill_nami_sui,
|
||||
}
|
||||
if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
|
||||
self._special_tempest()
|
||||
@@ -492,6 +536,7 @@ class WorldSkillMixin:
|
||||
'luna_uncap': self._luna_uncap,
|
||||
'skill_kanae_uncap': self._skill_kanae_uncap,
|
||||
'skill_eto_hoppe': self._skill_eto_hoppe,
|
||||
'skill_intruder': self._skill_intruder,
|
||||
}
|
||||
if self.character_used.skill_id_displayed in factory_dict:
|
||||
factory_dict[self.character_used.skill_id_displayed]()
|
||||
@@ -630,9 +675,11 @@ class WorldSkillMixin:
|
||||
kanae 觉醒技能,保存世界模式 progress 并在下次结算
|
||||
直接加减在 progress 最后
|
||||
技能存储 base_progress * PROG / 50,下一次消耗全部存储值(无视技能和搭档,但需要非技能隐藏状态)
|
||||
6.0 更新:需要体力消耗才存
|
||||
'''
|
||||
self.kanae_stored_progress = self.progress_normalized
|
||||
self.user.current_map.reclimb(self.final_progress)
|
||||
if self.user.current_map.stamina_cost > 0:
|
||||
self.kanae_stored_progress = self.progress_normalized
|
||||
self.user.current_map.reclimb(self.final_progress)
|
||||
|
||||
def _skill_eto_hoppe(self) -> None:
|
||||
'''
|
||||
@@ -642,6 +689,67 @@ class WorldSkillMixin:
|
||||
self.character_bonus_progress_normalized = self.progress_normalized
|
||||
self.user.current_map.reclimb(self.final_progress)
|
||||
|
||||
def _skill_chinatsu(self) -> None:
|
||||
'''
|
||||
chinatsu 技能,hp 超过时提高搭档能力值
|
||||
'''
|
||||
_flag = self.user_play.skill_chinatsu_flag
|
||||
if not self.user_play.hp_interval_bonus or not _flag:
|
||||
return
|
||||
|
||||
x = _flag[:min(len(_flag), self.user_play.hp_interval_bonus)]
|
||||
self.over_skill_increase = x.count('2') * 5
|
||||
self.prog_skill_increase = x.count('1') * 5
|
||||
|
||||
def _skill_intruder(self) -> None:
|
||||
'''
|
||||
intruder 技能,夺舍后世界进度翻倍
|
||||
'''
|
||||
if self.user_play.invasion_flag:
|
||||
self.character_bonus_progress_normalized = self.progress_normalized
|
||||
self.user.current_map.reclimb(self.final_progress)
|
||||
|
||||
def _skill_salt(self) -> None:
|
||||
'''
|
||||
salt 技能,根据单个章节地图的完成情况额外获得最高 10 的世界模式进度
|
||||
|
||||
当前章节完成地图数 / 本章节总地图数(不含无限图)* 10
|
||||
'''
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
self.character_bonus_progress_normalized = 10
|
||||
return
|
||||
|
||||
kvd = UserKVTable(self.c, self.user.user_id, 'world')
|
||||
|
||||
chapter_id = self.user.current_map.chapter
|
||||
count = kvd['chapter_complete_count', chapter_id] or 0
|
||||
total = len(MapParser.chapter_info_without_repeatable[chapter_id])
|
||||
if count > total:
|
||||
count = total
|
||||
|
||||
radio = count / total if total else 1
|
||||
|
||||
self.character_bonus_progress_normalized = 10 * radio
|
||||
|
||||
def _skill_hikari_selene(self) -> None:
|
||||
'''
|
||||
hikari_selene 技能,曲目结算时每满一格收集条增加 2 step 与 2 overdrive
|
||||
'''
|
||||
self.over_skill_increase = 0
|
||||
self.prog_skill_increase = 0
|
||||
if 0 < self.user_play.health <= 100:
|
||||
self.over_skill_increase = int(self.user_play.health / 10) * 2
|
||||
self.prog_skill_increase = int(self.user_play.health / 10) * 2
|
||||
|
||||
def _skill_nami_sui(self) -> None:
|
||||
'''
|
||||
nami & sui 技能,根据纯粹音符数与 FEVER 等级提高世界模式进度
|
||||
'''
|
||||
if self.user_play.fever_bonus is None:
|
||||
return
|
||||
|
||||
self.character_bonus_progress_normalized = self.user_play.fever_bonus / 1000
|
||||
|
||||
|
||||
class BaseWorldPlay(WorldSkillMixin):
|
||||
'''
|
||||
@@ -690,9 +798,13 @@ class BaseWorldPlay(WorldSkillMixin):
|
||||
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
|
||||
'beyond_boost_gauge': self.user.beyond_boost_gauge,
|
||||
# 'wpaid': 'helloworld', # world play id ???
|
||||
# progress_before_sub_boost
|
||||
# progress_sub_boost_amount
|
||||
# subscription_multiply
|
||||
'progress_before_sub_boost': self.final_progress,
|
||||
'progress_sub_boost_amount': 0,
|
||||
# 'subscription_multiply'
|
||||
|
||||
# lephon_final: bool dynamic map info
|
||||
# lephon_active: bool dynamic map info
|
||||
# 'steps_modified': False,
|
||||
}
|
||||
|
||||
if self.character_used.skill_id_displayed == 'skill_maya':
|
||||
@@ -751,6 +863,11 @@ class BaseWorldPlay(WorldSkillMixin):
|
||||
if self.user_play.beyond_gauge == 0 and self.user.kanae_stored_prog > 0:
|
||||
# 实在不想拆开了,在这里判断一下,注意这段不会在 BeyondWorldPlay 中执行
|
||||
self.kanae_added_progress = self.user.kanae_stored_prog
|
||||
|
||||
if self.user_play.invasion_flag == 1 or (self.user_play.invasion_flag == 2 and self.user_play.health <= 0):
|
||||
# 这里硬编码了搭档 id 72
|
||||
self.character_used = UserCharacter(self.c, 72, self.user)
|
||||
self.character_used.select_character_info()
|
||||
else:
|
||||
self.character_used.character_id = self.user.character.character_id
|
||||
self.character_used.level.level = self.user.character.level.level
|
||||
@@ -785,6 +902,9 @@ class BaseWorldPlay(WorldSkillMixin):
|
||||
|
||||
self.user.current_map.update()
|
||||
|
||||
# 更新用户完成情况
|
||||
self.user.update_user_world_complete_info()
|
||||
|
||||
def update(self) -> None:
|
||||
'''世界模式更新'''
|
||||
self.before_update()
|
||||
@@ -932,6 +1052,7 @@ class BeyondWorldPlay(BaseWorldPlay):
|
||||
r['char_stats']['over_skill_increase'] = self.over_skill_increase
|
||||
|
||||
r["user_map"]["steps"] = len(self.user.current_map.steps_for_climbing)
|
||||
|
||||
r['affinity_multiply'] = self.affinity_multiplier
|
||||
if self.user_play.beyond_boost_gauge_usage != 0:
|
||||
r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
class InitData:
|
||||
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
|
||||
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe']
|
||||
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe', 'Forlorn(Ascendant - 6th Seeker)', 'Chinatsu', 'Tsumugi', 'Nai', 'Selene Sheryl (MIR-203)', 'Salt', 'Acid', 'Hikari & Selene Sheryl (Fracture & MIR-203)', 'Hikari & El Clear', 'Tairitsu & El Fail', 'Nami & Sui (Twilight)', 'Nonoka']
|
||||
|
||||
skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan', 'skill_luna_ilot', 'skill_eto_hoppe']
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid', 'skill_hikari_selene', 'skill_hikari_clear', 'skill_tairitsu_fail', 'skill_nami_sui', 'skill_nonoka']
|
||||
|
||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', '', '', '']
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
|
||||
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32,
|
||||
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50, 38, 22]
|
||||
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50, 38, 22, 63, 37, 23, 59, 45, 20, 43, 50, 22, 37, 26, 47]
|
||||
|
||||
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52,
|
||||
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45, 41, 12, 31]
|
||||
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 40, 50, 45, 41, 12, 31, 72, 40, 16, 33, 35, 23, 24, 46, 26, 49, 32, 35]
|
||||
|
||||
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18,
|
||||
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45, 41, 12, 19]
|
||||
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 35, 34, 45, 41, 12, 19, 38, 40, 26, 39, 56, 20, 25, 46, 18, 71, 29, 25]
|
||||
|
||||
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52,
|
||||
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50, 61.6, 48, 37]
|
||||
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 115, 70, 50, 61.6, 48, 37, 90, 60, 50, 92, 66, 44, 79, 50, 47, 55, 49, 79]
|
||||
|
||||
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73,
|
||||
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67, 41, 55, 50]
|
||||
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 80, 80, 67, 41, 55, 50, 103, 66, 35, 52, 65, 50, 43, 84, 55, 73, 59, 60]
|
||||
|
||||
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
|
||||
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90, 41, 34, 30]
|
||||
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 75, 54, 90, 41, 34, 30, 55, 66, 55, 62, 81, 44, 46, 84, 39, 105, 55, 43]
|
||||
|
||||
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
|
||||
65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6, 48, 37]
|
||||
65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 115, 80, 50, 61.6, 48, 37, 90, 60, 50, 102, 76, 44, 89, 50, 47, 55, 49, 79]
|
||||
|
||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
|
||||
80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41, 55, 50]
|
||||
80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 80, 90, 67, 41, 55, 50, 103, 66, 35, 62, 75, 50, 53, 84, 55, 73, 59, 60]
|
||||
|
||||
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
|
||||
56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41, 34, 30]
|
||||
56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 75, 64, 90, 41, 34, 30, 55, 66, 55, 72, 91, 44, 56, 84, 39, 105, 55, 43]
|
||||
|
||||
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0, 0, 0]
|
||||
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 4, 2, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0]
|
||||
|
||||
char_core = {
|
||||
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||
@@ -65,18 +65,22 @@ class InitData:
|
||||
30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}],
|
||||
34: [{'core_id': 'core_tanoc', 'amount': 15}],
|
||||
23: [{'core_id': 'core_desolate', 'amount': 5}, {'core_id': 'core_serene', 'amount': 25}],
|
||||
81: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
82: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
84: [{'core_id': 'core_maimai', 'amount': 15}],
|
||||
}
|
||||
|
||||
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
|
||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene']
|
||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene', 'core_maimai']
|
||||
|
||||
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy', 'hailstone', 'furetemitai']
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy', 'hailstone', 'furetemitai', 'prayer', 'astralexe', 'trpno', 'blackmirror', 'tau', 'snowwhite3', 'altale3', 'energysynergymatrix3', 'anokumene3', 'nhelv3', 'wontbackdown', 'someday']
|
||||
|
||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"]
|
||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_chap8", "scenery_beyond"]
|
||||
|
||||
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
||||
# TODO: online_banners
|
||||
|
||||
role = ['system', 'admin', 'user', 'selecter']
|
||||
role_caption = ['系统', '管理员', '用户', '查询接口']
|
||||
|
||||
@@ -445,5 +445,50 @@
|
||||
"core_generic_20",
|
||||
"course_banner_11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "6.0-dan-12",
|
||||
"style": 12,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "abstrusedilemma"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "worldender"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 4,
|
||||
"id": "alterego"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "designant"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-11",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 12",
|
||||
"course_name": "侵蚀万物的色彩",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment2000",
|
||||
"core_generic_20",
|
||||
"course_banner_12"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -862,5 +862,149 @@
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "ongeki_append_2",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "ongeki_append_2",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
},
|
||||
{
|
||||
"name": "lephon",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "lephon",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "chunithm_append_3",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "chunithm_append_3",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
},
|
||||
{
|
||||
"name": "maimai_append_2",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "maimai_append_2",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
},
|
||||
{
|
||||
"name": "undertale",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "undertale",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "djmax",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "djmax",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "djmax_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "djmax_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "anima",
|
||||
"items": [
|
||||
{
|
||||
"id": "anima",
|
||||
"type": "pack",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"id": "core_generic",
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
}
|
||||
]
|
||||
@@ -1918,5 +1918,203 @@
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "thirdsun",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "thirdsun",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "crimsonquartz",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "crimsonquartz",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "diesirae",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "diesirae",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "spiritdauntless",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "spiritdauntless",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "placebobattler",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "placebobattler",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "megalovaniarmx",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "megalovaniarmx",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "shinchoku",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "shinchoku",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "dullblade",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "dullblade",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "tictactoe",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "tictactoe",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "dontdie",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "dontdie",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "miles",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "miles",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
}
|
||||
]
|
||||
@@ -36,7 +36,9 @@ stamina int,
|
||||
world_mode_locked_end_ts int,
|
||||
beyond_boost_gauge real default 0,
|
||||
kanae_stored_prog real default 0,
|
||||
mp_notification_enabled int default 1
|
||||
mp_notification_enabled int default 1,
|
||||
insight_state default 4, -- 这里写 4 是为了避免不能切换状态和无故被侵入
|
||||
custom_banner text
|
||||
);
|
||||
create table if not exists login(access_token text,
|
||||
user_id int,
|
||||
@@ -145,7 +147,9 @@ stamina_multiply int,
|
||||
fragment_multiply int,
|
||||
prog_boost_multiply int,
|
||||
beyond_boost_gauge_usage int,
|
||||
skill_cytusii_flag text
|
||||
skill_cytusii_flag text,
|
||||
skill_chinatsu_flag text,
|
||||
invasion_flag int
|
||||
);
|
||||
create table if not exists item(item_id text,
|
||||
type text,
|
||||
@@ -276,6 +280,17 @@ status int,
|
||||
primary key(user_id, mission_id)
|
||||
);
|
||||
|
||||
-- value 无类型
|
||||
create table if not exists user_kvdata(
|
||||
user_id int,
|
||||
class text,
|
||||
key text,
|
||||
idx int,
|
||||
value,
|
||||
primary key(user_id, class, key, idx)
|
||||
);
|
||||
|
||||
|
||||
create index if not exists best_score_1 on best_score (song_id, difficulty);
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
|
||||
@@ -247,6 +247,10 @@ class CommandParser:
|
||||
self.room.make_round()
|
||||
logging.info(f'Room `{self.room.room_code}` starts playing')
|
||||
|
||||
for p in self.room.players:
|
||||
# 防止提前结算
|
||||
p.finish_flag = 0
|
||||
|
||||
if self.room.state == 4:
|
||||
# 这好像会误判
|
||||
# if player.download_percent < 99:
|
||||
|
||||
@@ -23,7 +23,7 @@ def get_bps():
|
||||
return s
|
||||
|
||||
bp = Blueprint('server', __name__)
|
||||
list(map(bp.register_blueprint, [user.bp, auth.bp, friend.bp, score.bp,
|
||||
list(map(bp.register_blueprint, [user.bp2, user.bp, auth.bp, friend.bp, score.bp,
|
||||
world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp, mission.bp]))
|
||||
|
||||
bps = [Blueprint(x, __name__, url_prefix=x)
|
||||
|
||||
@@ -36,12 +36,6 @@ def login():
|
||||
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
||||
|
||||
|
||||
@bp.route('/verify', methods=['POST']) # 邮箱验证进度查询
|
||||
@arc_try
|
||||
def email_verify():
|
||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||
|
||||
|
||||
def auth_required(req):
|
||||
# arcaea登录验证,写成了修饰器
|
||||
def decorator(view):
|
||||
|
||||
@@ -6,7 +6,7 @@ from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from core.bundle import BundleDownload
|
||||
from core.download import DownloadList
|
||||
from core.error import RateLimit
|
||||
from core.error import RateLimit, ArcError
|
||||
from core.item import ItemCharacter
|
||||
from core.notification import NotificationFactory
|
||||
from core.sql import Connect
|
||||
@@ -101,6 +101,28 @@ def finale_end(user_id):
|
||||
return success_return({})
|
||||
|
||||
|
||||
@bp.route('/insight/me/complete/<string:pack_id>', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def insight_complete(user_id, pack_id):
|
||||
# insight state change
|
||||
with Connect() as c:
|
||||
u = UserOnline(c, user_id)
|
||||
if pack_id == 'eden_append_1':
|
||||
item = ItemCharacter(c)
|
||||
item.set_id('72') # Insight (Ascendant - 8th Seeker)
|
||||
item.user_claim_item(u)
|
||||
u.update_user_one_column('insight_state', 1)
|
||||
elif pack_id == 'lephon':
|
||||
u.update_user_one_column('insight_state', 3)
|
||||
else:
|
||||
raise ArcError('Invalid pack_id', 151, status=404)
|
||||
|
||||
return success_return({
|
||||
'insight_state': u.insight_state
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/applog/me/log', methods=['POST'])
|
||||
def applog_me():
|
||||
# 异常日志,不处理
|
||||
|
||||
@@ -29,11 +29,12 @@ def score_token_world(user_id):
|
||||
|
||||
d = request.args.get
|
||||
|
||||
stamina_multiply = int(d('stamina_multiply', 1))
|
||||
fragment_multiply = int(d('fragment_multiply', 100))
|
||||
prog_boost_multiply = int(d('prog_boost_multiply', 0))
|
||||
beyond_boost_gauge_use = int(d('beyond_boost_gauge_use', 0))
|
||||
stamina_multiply = d('stamina_multiply', 1, type=int)
|
||||
fragment_multiply = d('fragment_multiply', 100, type=int)
|
||||
prog_boost_multiply = d('prog_boost_multiply', 0, type=int)
|
||||
beyond_boost_gauge_use = d('beyond_boost_gauge_use', 0, type=int)
|
||||
skill_cytusii_flag = None
|
||||
skill_chinatsu_flag = None
|
||||
skill_id = d('skill_id')
|
||||
|
||||
if (skill_id == 'skill_ilith_ivy' or skill_id == 'skill_hikari_vanessa') and d('is_skill_sealed') == 'false':
|
||||
@@ -41,23 +42,34 @@ def score_token_world(user_id):
|
||||
# TODO: 需要重构整个 user_play,世界模式 / 课题模式,所以现在临时 work 一下
|
||||
skill_cytusii_flag = ''.join([str(randint(0, 2)) for _ in range(5)])
|
||||
|
||||
if skill_id == 'skill_chinatsu' and d('is_skill_sealed') == 'false':
|
||||
skill_chinatsu_flag = ''.join([str(randint(0, 2)) for _ in range(7)])
|
||||
|
||||
skill_flag = skill_cytusii_flag or skill_chinatsu_flag
|
||||
|
||||
with Connect() as c:
|
||||
x = UserPlay(c, UserOnline(c, user_id))
|
||||
x.song.set_chart(request.args['song_id'], int(
|
||||
request.args['difficulty']))
|
||||
x.song.set_chart(d('song_id'), d('difficulty', type=int))
|
||||
x.set_play_state_for_world(
|
||||
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag)
|
||||
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag, skill_chinatsu_flag)
|
||||
|
||||
r = {
|
||||
"stamina": x.user.stamina.stamina,
|
||||
"max_stamina_ts": x.user.stamina.max_stamina_ts,
|
||||
"token": x.song_token,
|
||||
'play_parameters': {},
|
||||
}
|
||||
if skill_cytusii_flag and skill_id:
|
||||
if skill_flag and skill_id:
|
||||
r['play_parameters'] = {
|
||||
skill_id: list(
|
||||
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_cytusii_flag))
|
||||
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_flag)),
|
||||
}
|
||||
|
||||
if x.invasion_flag == 1:
|
||||
r['play_parameters']['invasion_start'] = True
|
||||
elif x.invasion_flag == 2:
|
||||
r['play_parameters']['invasion_hard'] = True
|
||||
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@@ -115,11 +127,17 @@ def song_score_post(user_id):
|
||||
x.submission_hash = request.form['submission_hash']
|
||||
if 'combo_interval_bonus' in request.form:
|
||||
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
|
||||
if 'hp_interval_bonus' in request.form:
|
||||
x.hp_interval_bonus = int(request.form['hp_interval_bonus'])
|
||||
# visible_map_count
|
||||
if 'fever_bonus' in request.form:
|
||||
x.fever_bonus = int(request.form['fever_bonus'])
|
||||
x.highest_health = request.form.get("highest_health", type=int)
|
||||
x.lowest_health = request.form.get("lowest_health", type=int)
|
||||
if not x.is_valid:
|
||||
raise InputError('Invalid score.', 107)
|
||||
x.upload_score()
|
||||
# room_code???
|
||||
return success_return(x.to_dict())
|
||||
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ from .func import arc_try, header_check, success_return
|
||||
|
||||
bp = Blueprint('user', __name__, url_prefix='/user')
|
||||
|
||||
bp2 = Blueprint('account', __name__, url_prefix='/account')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST']) # 注册接口
|
||||
@bp2.route('', methods=['POST'])
|
||||
@arc_try
|
||||
def register():
|
||||
error = header_check(request)
|
||||
@@ -50,6 +53,16 @@ def user_me(user_id):
|
||||
return success_return(UserOnline(c, user_id).to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/toggle_invasion', methods=['POST']) # insight skill
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def toggle_invasion(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.toggle_invasion()
|
||||
return success_return({'user_id': user.user_id, 'insight_state': user.insight_state})
|
||||
|
||||
|
||||
@bp.route('/me/character', methods=['POST']) # 角色切换
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
@@ -163,6 +176,7 @@ def sys_set(user_id, set_arg):
|
||||
|
||||
|
||||
@bp.route('/me/request_delete', methods=['POST']) # 删除账号
|
||||
@bp2.route('/me/request_delete', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def user_delete(user_id):
|
||||
@@ -173,6 +187,14 @@ def user_delete(user_id):
|
||||
|
||||
|
||||
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
||||
@bp2.route('/email/resend_verify', methods=['POST'])
|
||||
@arc_try
|
||||
def email_resend_verify():
|
||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||
|
||||
|
||||
@bp.route('/verify', methods=['POST']) # 邮箱验证状态查询
|
||||
@bp2.route('/verify', methods=['POST'])
|
||||
@arc_try
|
||||
def email_verify():
|
||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<option value="1">Present</option>
|
||||
<option value="2">Future</option>
|
||||
<option value="3">Beyond</option>
|
||||
<option value="4">Eternal</option>
|
||||
</select>
|
||||
<br />
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<option value="1">Present</option>
|
||||
<option value="2">Future</option>
|
||||
<option value="3">Beyond</option>
|
||||
<option value="4">Eternal</option>
|
||||
</select>
|
||||
<br />
|
||||
<input type="submit" value="Find">
|
||||
@@ -33,6 +34,8 @@
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif difficulty == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% elif difficulty == 4 %}
|
||||
<span class="difficulty_etr">ETR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif post['difficulty'] == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% elif post['difficulty'] == 4 %}
|
||||
<span class="difficulty_etr">ETR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif user['difficulty'] == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% elif user['difficulty'] == 4 %}
|
||||
<span class="difficulty_etr">ETR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
{% endif %}
|
||||
@@ -176,6 +178,8 @@
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif i['difficulty'] == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% elif i['difficulty'] == 4 %}
|
||||
<span class="difficulty_etr">ETR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -443,7 +443,7 @@ def all_character():
|
||||
def change_character():
|
||||
# 修改角色数据
|
||||
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan']
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid', 'skill_hikari_selene', 'skill_hikari_clear', 'skill_tairitsu_fail', 'skill_nami_sui']
|
||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user