mirror of
https://gitea.tendokyu.moe/Hay1tsme/artemis.git
synced 2026-08-28 14:54:16 -05:00
Merge pull request '[chuni] [webui] character illustrations support' (#240) from daydensteve/artemis_chuni_webui_improvements:chuni_webui_chara_illust into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/240
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""chuni_chara_illust_support
|
||||
|
||||
Revision ID: 914dea1204e8
|
||||
Revises: 318d52559e83
|
||||
Create Date: 2026-01-10 21:55:43.651414
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '914dea1204e8'
|
||||
down_revision = 'd478fe5b757f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# new table to store all character illustrations
|
||||
op.create_table('chuni_static_character_illust',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False),
|
||||
sa.Column('characterId', sa.Integer(), nullable=False),
|
||||
sa.Column('illustId', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('requiredRank', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('imagePath1', sa.String(length=255), nullable=True),
|
||||
sa.Column('imagePath2', sa.String(length=255), nullable=True),
|
||||
sa.Column('imagePath3', sa.String(length=255), nullable=True),
|
||||
sa.Column('opt', sa.BIGINT(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['opt'], ['chuni_static_opt.id'], onupdate='cascade', ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('version', 'characterId', 'illustId', name='chuni_static_character_illust_uk'),
|
||||
mysql_charset='utf8mb4'
|
||||
)
|
||||
|
||||
# new table to store all character works information
|
||||
op.create_table('chuni_static_character_works',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False),
|
||||
sa.Column('worksId', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('sortName', sa.String(length=255), nullable=False),
|
||||
sa.Column('releaseVersion', sa.Integer(), nullable=False),
|
||||
sa.Column('priority', sa.Integer(), nullable=True),
|
||||
sa.Column('opt', sa.BIGINT(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['opt'], ['chuni_static_opt.id'], onupdate='cascade', ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('version', 'worksId', name='chuni_static_character_works_uk'),
|
||||
mysql_charset='utf8mb4'
|
||||
)
|
||||
|
||||
# update character table to just reference the works table by ID now (instead of storing its name)
|
||||
op.add_column('chuni_static_character', sa.Column('worksId', sa.Integer(), server_default='-1', nullable=True))
|
||||
op.drop_column('chuni_static_character', 'worksName')
|
||||
|
||||
# remove image paths from character table since they are now redundant with the illustrations table
|
||||
op.drop_column('chuni_static_character', 'imagePath1')
|
||||
op.drop_column('chuni_static_character', 'imagePath2')
|
||||
op.drop_column('chuni_static_character', 'imagePath3')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('chuni_static_character', sa.Column('worksName', mysql.VARCHAR(length=255), nullable=True))
|
||||
op.drop_column('chuni_static_character', 'worksId')
|
||||
|
||||
op.add_column('chuni_static_character', sa.Column('imagePath1', mysql.VARCHAR(length=255), nullable=True))
|
||||
op.add_column('chuni_static_character', sa.Column('imagePath2', mysql.VARCHAR(length=255), nullable=True))
|
||||
op.add_column('chuni_static_character', sa.Column('imagePath3', mysql.VARCHAR(length=255), nullable=True))
|
||||
|
||||
op.drop_table('chuni_static_character_works')
|
||||
op.drop_table('chuni_static_character_illust')
|
||||
|
||||
@@ -16,14 +16,13 @@ mods:
|
||||
|
||||
# Allow use of all available customization items in frontend web ui
|
||||
# note: This effectively makes every available item appear to be in the user's inventory. It does _not_ override the "disableFlag" setting on individual items
|
||||
# warning: This can result in pushing a lot of data, especially the userbox items. Recommended for local network use only.
|
||||
forced_item_unlocks:
|
||||
map_icons: False
|
||||
system_voices: False
|
||||
avatar_accessories: False
|
||||
nameplates: False
|
||||
trophies: False
|
||||
character_icons: False
|
||||
characters: False
|
||||
stages: False
|
||||
|
||||
version:
|
||||
|
||||
@@ -12,7 +12,7 @@ from core.frontend import FE_Base, UserSession
|
||||
from core.config import CoreConfig
|
||||
from .database import ChuniData
|
||||
from .config import ChuniConfig
|
||||
from .const import ChuniConstants, AvatarCategory, ItemKind
|
||||
from .const import ChuniConstants, AvatarCategory, FavoriteItemKind, ItemKind
|
||||
from .read import ChuniReader
|
||||
|
||||
|
||||
@@ -479,28 +479,92 @@ class ChuniFrontend(FE_Base):
|
||||
|
||||
return (items, len(rows))
|
||||
|
||||
async def get_available_characters(self, version: int, profile: Row) -> Tuple[List[Dict], int]:
|
||||
async def get_available_characters(self, version: int, user_id: int) -> Tuple[List[Dict], int, int]:
|
||||
"""
|
||||
Get a set of ALL characters currently available to the user, including "default have" characters that have yet to be used, with available illustrations.
|
||||
@note This is a bit of a bear to piece together as it requires data from several different tables
|
||||
"""
|
||||
# Figure out if we're force unlocked
|
||||
force_unlocked = self.game_cfg.mods.forced_item_unlocks("characters")
|
||||
items = dict()
|
||||
rows = await self.data.static.get_characters(version)
|
||||
if rows is None:
|
||||
return (items, 0) # can only happen with old db
|
||||
|
||||
force_unlocked = self.game_cfg.mods.forced_item_unlocks("character_icons")
|
||||
|
||||
user_characters = []
|
||||
if not force_unlocked:
|
||||
user_characters = await self.data.item.get_characters(profile.user)
|
||||
user_characters = [chara["characterId"] for chara in user_characters] + [profile.characterId]
|
||||
all_characters = await self.data.static.get_characters(version)
|
||||
if all_characters is None:
|
||||
return (items, 0, 0) # can only happen with old db
|
||||
|
||||
for row in rows:
|
||||
if force_unlocked or row["defaultHave"] or row["characterId"] in user_characters:
|
||||
item = dict()
|
||||
item["id"] = row["characterId"]
|
||||
item["name"] = row["name"]
|
||||
item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".webp"
|
||||
items[row["characterId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
# Identify all characters the user has used.
|
||||
# We need these rows to know current ranks
|
||||
user_ranks = dict()
|
||||
characters = await self.data.item.get_characters(user_id, version)
|
||||
for chara in characters:
|
||||
id = chara["characterId"]
|
||||
user_ranks[id] = chara["level"]
|
||||
|
||||
# Identify all character favorites
|
||||
user_favs = await self.data.item.get_all_favorites( user_id, version, fav_kind=FavoriteItemKind.CHARACTER)
|
||||
# convert to easy-to-reference set
|
||||
user_favs = [f["favId"] for f in user_favs]
|
||||
|
||||
# Build the full set of available characters
|
||||
for chara in all_characters:
|
||||
has_character = chara["characterId"] in user_ranks.keys()
|
||||
# if we are force unlock, just take literally everything
|
||||
# if not, only take enabled characters that are either available by default or in the users characters items list
|
||||
if force_unlocked or chara["defaultHave"] or has_character:
|
||||
# found a character the user has access to
|
||||
id = chara["characterId"]
|
||||
items[id] = {
|
||||
"id": id,
|
||||
"name": chara["name"],
|
||||
"rank": user_ranks[id] if has_character else 1, # default rank to 1
|
||||
"isFav": id in user_favs,
|
||||
"illusts": dict()
|
||||
}
|
||||
|
||||
# Get a full list of character illustrations and map them to the character we've identified as usable
|
||||
all_illusts = await self.data.static.get_character_illusts(version)
|
||||
for illust in all_illusts:
|
||||
chara_id = illust["characterId"]
|
||||
if chara_id not in items.keys():
|
||||
# this character is not available to the user
|
||||
continue
|
||||
|
||||
illust_id = illust["illustId"]
|
||||
required_rank = illust["requiredRank"]
|
||||
|
||||
# If we're ignoring the rank or have met the rank requirement, include the illustration
|
||||
# @note The ignore_rank knob allows us to return results for whats currently available
|
||||
# (e.g. for use on the userbox ui) and all possible (e.g. for use on the characters ui).
|
||||
if force_unlocked or items[chara_id]["rank"] >= required_rank:
|
||||
items[chara_id]["illusts"][illust_id] = {
|
||||
"id": illust_id,
|
||||
"name": illust["name"],
|
||||
"requiredRank": required_rank,
|
||||
"iconPath": path.splitext(illust["imagePath3"])[0] + ".webp"
|
||||
}
|
||||
|
||||
# character data, total number of characters, total number of illustrations
|
||||
return (items, len(all_characters), len(all_illusts))
|
||||
|
||||
async def get_available_character_illusts(self, version: int, user_id: int) -> Tuple[List[Dict], int]:
|
||||
# Get a full list of characters
|
||||
characters, _, num_illusts = await self.get_available_characters(version, user_id)
|
||||
|
||||
# flatten the illustration data into a single new dict. This format works better for populating the userbox ui
|
||||
items = dict()
|
||||
for c in characters:
|
||||
chara = characters[c]
|
||||
for i in chara["illusts"]:
|
||||
illust = chara["illusts"][i]
|
||||
id = illust["id"]
|
||||
items[id] = {
|
||||
"charaId": chara["id"],
|
||||
"illustId": id,
|
||||
"name": illust["name"],
|
||||
"iconPath": illust["iconPath"],
|
||||
"isFav": chara["isFav"]
|
||||
}
|
||||
|
||||
return (items, num_illusts)
|
||||
|
||||
async def get_available_avatar_items(self, version: int, category: AvatarCategory, user_unlocked_items: List[int]) -> Tuple[List[Dict], int]:
|
||||
items = dict()
|
||||
@@ -542,7 +606,7 @@ class ChuniFrontend(FE_Base):
|
||||
# Build up lists of available userbox components
|
||||
nameplates, total_nameplates = await self.get_available_nameplates(version, profile)
|
||||
trophies, total_trophies = await self.get_available_trophies(version, profile)
|
||||
characters, total_characters = await self.get_available_characters(version, profile)
|
||||
characters, total_characters = await self.get_available_character_illusts(version, user_id)
|
||||
|
||||
# Get the user's team
|
||||
team_name = "ARTEMiS"
|
||||
@@ -707,15 +771,31 @@ class ChuniFrontend(FE_Base):
|
||||
new_trophy_sub_1: str = form_data.get("trophySub1")
|
||||
new_trophy_sub_2: str = form_data.get("trophySub2")
|
||||
new_character: str = form_data.get("character")
|
||||
new_character_illust: str = form_data.get("characterIllust")
|
||||
|
||||
if not new_nameplate or \
|
||||
not new_trophy or \
|
||||
not new_trophy_sub_1 or \
|
||||
not new_trophy_sub_2 or \
|
||||
not new_character:
|
||||
not new_character or \
|
||||
not new_character_illust:
|
||||
return RedirectResponse("/game/chuni/userbox?e=4", 303)
|
||||
|
||||
if not await self.data.profile.update_userbox(usr_sesh.user_id, usr_sesh.chunithm_version, new_nameplate, new_trophy, new_trophy_sub_1, new_trophy_sub_2, new_character):
|
||||
# update the userbox itself
|
||||
if not await self.data.profile.update_userbox(usr_sesh.user_id,
|
||||
usr_sesh.chunithm_version,
|
||||
new_nameplate,
|
||||
new_trophy,
|
||||
new_trophy_sub_1,
|
||||
new_trophy_sub_2,
|
||||
new_character,
|
||||
new_character_illust):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
# update the assigned character illustration
|
||||
if not await self.data.item.update_character_assigned_illust(usr_sesh.user_id,
|
||||
new_character,
|
||||
new_character_illust):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse("/game/chuni/userbox", 303)
|
||||
|
||||
@@ -65,6 +65,7 @@ class ChuniReader(BaseReader):
|
||||
await self.read_nameplate(f"{dir}/namePlate", this_opt_id)
|
||||
await self.read_trophy(f"{dir}/trophy", this_opt_id)
|
||||
await self.read_character(f"{dir}/chara", dds_images, this_opt_id)
|
||||
await self.read_character_works(f"{dir}/charaWorks", this_opt_id)
|
||||
await self.read_map_icon(f"{dir}/mapIcon", this_opt_id)
|
||||
await self.read_system_voice(f"{dir}/systemVoice", this_opt_id)
|
||||
await self.read_unlock_challenge(f"{dir}/unlockChallenge")
|
||||
@@ -360,6 +361,37 @@ class ChuniReader(BaseReader):
|
||||
else:
|
||||
self.logger.warning(f"Failed to insert trophy {id}")
|
||||
|
||||
async def read_character_illust(self, dds_images: dict, chara_id: str, illust_id: str, dds_id: str, name: str,
|
||||
required_rank: str, opt_id: Optional[int] = None) -> None:
|
||||
if dds_id in dds_images.keys():
|
||||
(imageDir, imagePaths) = dds_images[dds_id]
|
||||
|
||||
# image1 is full character (used during gameplay)
|
||||
# image2 is zoomed version (used in character select/transform)
|
||||
# image3 is icon (used character transform and userbox)
|
||||
imagePath1 = imagePaths[0] if len(imagePaths) > 0 else ""
|
||||
imagePath2 = imagePaths[1] if len(imagePaths) > 1 else ""
|
||||
imagePath3 = imagePaths[2] if len(imagePaths) > 2 else ""
|
||||
|
||||
# @note 2nd and 3rd images are used in the webui
|
||||
if imagePath2:
|
||||
self.copy_image(imagePath2, imageDir, "titles/chuni/img/character/")
|
||||
if imagePath3:
|
||||
self.copy_image(imagePath3, imageDir, "titles/chuni/img/character/")
|
||||
elif not (imagePath2 and imagePath3):
|
||||
self.logger.warning(f"Character {chara_id} illustration {illust_id} only has {len(imagePaths)} images. Expected 3")
|
||||
else:
|
||||
self.logger.warning(f"Unable to location character {chara_id} illustration {illust_id} images")
|
||||
|
||||
result = await self.data.static.put_character_illust(
|
||||
self.version, chara_id, illust_id, name, required_rank, imagePath1, imagePath2, imagePath3, opt_id
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
self.logger.info(f"Inserted character {chara_id} illustration {illust_id}")
|
||||
else:
|
||||
self.logger.warning(f"Failed to insert character {chara_id} illustration {illust_id}")
|
||||
|
||||
async def read_character(self, chara_dir: str, dds_images: dict, opt_id: Optional[int] = None) -> None:
|
||||
for root, dirs, files in walk(chara_dir):
|
||||
for dir in dirs:
|
||||
@@ -376,7 +408,7 @@ class ChuniReader(BaseReader):
|
||||
name = name.find("str").text
|
||||
sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
|
||||
for work in xml_root.findall("works"):
|
||||
worksName = work.find("str").text
|
||||
worksId = work.find("id").text
|
||||
rareType = xml_root.find("rareType").text
|
||||
defaultHave = xml_root.find("defaultHave").text == 'true'
|
||||
disableFlag = xml_root.find("disableFlag") # may not exist in older data
|
||||
@@ -384,22 +416,25 @@ class ChuniReader(BaseReader):
|
||||
|
||||
# character images are not stored alongside
|
||||
for image in xml_root.findall("defaultImages"):
|
||||
imageKey = image.find("str").text
|
||||
if imageKey in dds_images.keys():
|
||||
(imageDir, imagePaths) = dds_images[imageKey]
|
||||
imagePath1 = imagePaths[0] if len(imagePaths) > 0 else ""
|
||||
imagePath2 = imagePaths[1] if len(imagePaths) > 1 else ""
|
||||
imagePath3 = imagePaths[2] if len(imagePaths) > 2 else ""
|
||||
# @note the third image is the image needed for the user box ui
|
||||
if imagePath3:
|
||||
self.copy_image(imagePath3, imageDir, "titles/chuni/img/character/")
|
||||
else:
|
||||
self.logger.warning(f"Character {id} only has {len(imagePaths)} images. Expected 3")
|
||||
else:
|
||||
self.logger.warning(f"Unable to location character {id} images")
|
||||
ddsId = image.find("str").text
|
||||
await self.read_character_illust(dds_images, id, id, ddsId, name, 0, opt_id)
|
||||
|
||||
for i in range(1,10):
|
||||
addImage = xml_root.find(f"addImages{i}")
|
||||
if addImage:
|
||||
# note checking <changeImg> is redundant with just checking that the ID isn't -1
|
||||
illustId = addImage.find("image").find("id").text
|
||||
if illustId == '-1':
|
||||
# entry not populated. Skip
|
||||
continue
|
||||
|
||||
ddsId = addImage.find("image").find("str").text
|
||||
illustName = addImage.find("charaName").find("str").text
|
||||
reqRank = addImage.find("rank").text
|
||||
await self.read_character_illust(dds_images, id, illustId, ddsId, illustName, reqRank, opt_id)
|
||||
|
||||
result = await self.data.static.put_character(
|
||||
self.version, id, name, sortName, worksName, rareType, imagePath1, imagePath2, imagePath3, is_enabled, defaultHave, opt_id
|
||||
self.version, id, name, sortName, worksId, rareType, is_enabled, defaultHave, opt_id
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
@@ -407,6 +442,31 @@ class ChuniReader(BaseReader):
|
||||
else:
|
||||
self.logger.warning(f"Failed to insert character {id}")
|
||||
|
||||
async def read_character_works(self, charaworks_dir: str, opt_id: Optional[int] = None) -> None:
|
||||
for root, dirs, files in walk(charaworks_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/CharaWorks.xml"):
|
||||
with open(f"{root}/{dir}/CharaWorks.xml", "r", encoding='utf-8') as fp:
|
||||
strdata = fp.read()
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
id = name.find("id").text
|
||||
name = name.find("str").text
|
||||
sortName = name if xml_root.find("sortName") is None else xml_root.find("sortName").text
|
||||
priority = xml_root.find("priority").text
|
||||
for release in xml_root.findall("releaseTagName"):
|
||||
releaseVersion = int(release.find("id").text)
|
||||
|
||||
result = await self.data.static.put_character_works(
|
||||
self.version, id, name, sortName, releaseVersion, priority, opt_id
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
self.logger.info(f"Inserted characterWorks {id}")
|
||||
else:
|
||||
self.logger.warning(f"Failed to insert characterWorks {id}")
|
||||
|
||||
async def read_map_icon(self, mapicon_dir: str, opt_id: Optional[int] = None) -> None:
|
||||
for root, dirs, files in walk(mapicon_dir):
|
||||
for dir in dirs:
|
||||
|
||||
@@ -11,10 +11,11 @@ from sqlalchemy import (
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.sql import func, select, update
|
||||
from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer, String
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
from titles.chuni.schema.static import character as StaticCharaTable
|
||||
|
||||
character: Table = Table(
|
||||
"chuni_item_character",
|
||||
@@ -570,10 +571,31 @@ class ChuniItemData(BaseData):
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def update_character_assigned_illust(self, user_id: int, character_id: int, illust_id: int) -> Optional[int]:
|
||||
# Only do an update, not insert. Any alt illustration the user actually has is the result of a rank increase,
|
||||
# which means the row exists already. Adding a row would only end up being necessary if the webui was using
|
||||
# the force unlock characters mod. Do not mess with the actual acquired items data.
|
||||
sql = (
|
||||
update(character)
|
||||
.where(character.c.user == user_id, character.c.characterId == character_id)
|
||||
.values(assignIllust = illust_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_characters(
|
||||
self, user_id: int, limit: Optional[int] = None, offset: Optional[int] = None
|
||||
self, user_id: int, version: Optional[int] = None, limit: Optional[int] = None, offset: Optional[int] = None
|
||||
) -> Optional[List[Row]]:
|
||||
sql = select(character).where(character.c.user == user_id)
|
||||
conditions = []
|
||||
conditions.append(character.c.user == user_id)
|
||||
filters = []
|
||||
if version:
|
||||
filters.append(character.c.characterId == StaticCharaTable.c.characterId)
|
||||
conditions.append(StaticCharaTable.c.version == version)
|
||||
sql = select(character).filter(and_(*filters)).where(and_(*conditions))
|
||||
|
||||
if limit is not None or offset is not None:
|
||||
sql = sql.order_by(character.c.id)
|
||||
|
||||
@@ -488,13 +488,14 @@ class ChuniProfileData(BaseData):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def update_userbox(self, user_id: int, version: int, new_nameplate: int, new_trophy: int, new_trophy_sub_1: int, new_trophy_sub_2: int, new_character: int) -> bool:
|
||||
async def update_userbox(self, user_id: int, version: int, new_nameplate: int, new_trophy: int, new_trophy_sub_1: int, new_trophy_sub_2: int, new_character: int, new_character_illust: int) -> bool:
|
||||
sql = profile.update((profile.c.user == user_id) & (profile.c.version == version)).values(
|
||||
nameplateId=new_nameplate,
|
||||
trophyId=new_trophy,
|
||||
trophyIdSub1=new_trophy_sub_1,
|
||||
trophyIdSub2=new_trophy_sub_2,
|
||||
charaIllustId=new_character
|
||||
characterId=new_character,
|
||||
charaIllustId=new_character_illust
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
|
||||
|
||||
@@ -121,11 +121,8 @@ character = Table(
|
||||
Column("characterId", Integer),
|
||||
Column("name", String(255)),
|
||||
Column("sortName", String(255)),
|
||||
Column("worksName", String(255)),
|
||||
Column("worksId", Integer, server_default="-1"),
|
||||
Column("rareType", Integer),
|
||||
Column("imagePath1", String(255)),
|
||||
Column("imagePath2", String(255)),
|
||||
Column("imagePath3", String(255)),
|
||||
Column("isEnabled", Boolean, server_default="1"),
|
||||
Column("defaultHave", Boolean, server_default="0"),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
@@ -133,6 +130,38 @@ character = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
character_illust = Table(
|
||||
"chuni_static_character_illust",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("characterId", Integer),
|
||||
Column("illustId", Integer),
|
||||
Column("name", String(255)),
|
||||
Column("requiredRank", Integer),
|
||||
Column("imagePath1", String(255)),
|
||||
Column("imagePath2", String(255)),
|
||||
Column("imagePath3", String(255)),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "characterId", "illustId", name="chuni_static_character_illust_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
character_works = Table(
|
||||
"chuni_static_character_works",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("worksId", Integer),
|
||||
Column("name", String(255)),
|
||||
Column("sortName", String(255)),
|
||||
Column("releaseVersion", Integer),
|
||||
Column("priority", Integer),
|
||||
Column("opt", BIGINT, ForeignKey("chuni_static_opt.id", ondelete="SET NULL", onupdate="cascade")),
|
||||
UniqueConstraint("version", "worksId", name="chuni_static_character_works_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
trophy = Table(
|
||||
"chuni_static_trophy",
|
||||
metadata,
|
||||
@@ -966,11 +995,8 @@ class ChuniStaticData(BaseData):
|
||||
characterId: int,
|
||||
name: str,
|
||||
sortName: str,
|
||||
worksName: str,
|
||||
worksId: int,
|
||||
rareType: int,
|
||||
imagePath1: str,
|
||||
imagePath2: str,
|
||||
imagePath3: str,
|
||||
isEnabled: int,
|
||||
defaultHave: int,
|
||||
opt_id: int = None
|
||||
@@ -980,11 +1006,8 @@ class ChuniStaticData(BaseData):
|
||||
characterId=characterId,
|
||||
name=name,
|
||||
sortName=sortName,
|
||||
worksName=worksName,
|
||||
worksId=worksId,
|
||||
rareType=rareType,
|
||||
imagePath1=imagePath1,
|
||||
imagePath2=imagePath2,
|
||||
imagePath3=imagePath3,
|
||||
isEnabled=isEnabled,
|
||||
defaultHave=defaultHave,
|
||||
opt=coalesce(character.c.opt, opt_id)
|
||||
@@ -993,11 +1016,8 @@ class ChuniStaticData(BaseData):
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
name=name,
|
||||
sortName=sortName,
|
||||
worksName=worksName,
|
||||
worksId=worksId,
|
||||
rareType=rareType,
|
||||
imagePath1=imagePath1,
|
||||
imagePath2=imagePath2,
|
||||
imagePath3=imagePath3,
|
||||
isEnabled=isEnabled,
|
||||
defaultHave=defaultHave,
|
||||
opt=coalesce(character.c.opt, opt_id)
|
||||
@@ -1029,6 +1049,127 @@ class ChuniStaticData(BaseData):
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_character_illust(
|
||||
self,
|
||||
version: int,
|
||||
characterId: int,
|
||||
illustId: int,
|
||||
name: str,
|
||||
requiredRank: int,
|
||||
imagePath1: str,
|
||||
imagePath2: str,
|
||||
imagePath3: str,
|
||||
opt_id: int = None
|
||||
) -> Optional[int]:
|
||||
sql = insert(character_illust).values(
|
||||
version=version,
|
||||
characterId=characterId,
|
||||
illustId=illustId,
|
||||
name=name,
|
||||
requiredRank=requiredRank,
|
||||
imagePath1=imagePath1,
|
||||
imagePath2=imagePath2,
|
||||
imagePath3=imagePath3,
|
||||
opt=coalesce(character_illust.c.opt, opt_id)
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
name=name,
|
||||
requiredRank=requiredRank,
|
||||
imagePath1=imagePath1,
|
||||
imagePath2=imagePath2,
|
||||
imagePath3=imagePath3,
|
||||
opt=coalesce(character_illust.c.opt, opt_id)
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_character_illusts(
|
||||
self, version: int, characterId: Optional[int] = None, characterRank: Optional[int] = None
|
||||
) -> Optional[List[Dict]]:
|
||||
"""
|
||||
Gets all character illustrations present in the given version.
|
||||
@warning This does not filter results down by enabled status like the character table query does.
|
||||
"""
|
||||
conditions = []
|
||||
# default to all illustrations for the given version
|
||||
conditions.append(character_illust.c.version == version)
|
||||
if characterId is not None:
|
||||
# limit results to the specified character
|
||||
conditions.append(character_illust.c.characterId == characterId)
|
||||
if characterRank is not None:
|
||||
# limit results to illustrations accessible with the given rank
|
||||
conditions.append(character_illust.c.requiredRank <= characterRank)
|
||||
|
||||
sql = (
|
||||
select(character_illust)
|
||||
.where(and_(*conditions))
|
||||
.order_by(character_illust.c.illustId)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_character_works(
|
||||
self,
|
||||
version: int,
|
||||
worksId: int,
|
||||
name: str,
|
||||
sortName: str,
|
||||
releaseVersion: int,
|
||||
priority: int,
|
||||
opt_id: int = None
|
||||
) -> Optional[int]:
|
||||
sql = insert(character_works).values(
|
||||
version=version,
|
||||
worksId=worksId,
|
||||
name=name,
|
||||
sortName=sortName,
|
||||
releaseVersion=releaseVersion,
|
||||
priority=priority,
|
||||
opt=coalesce(character_works.c.opt, opt_id)
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
name=name,
|
||||
sortName=sortName,
|
||||
releaseVersion=releaseVersion,
|
||||
priority=priority,
|
||||
opt=coalesce(character_works.c.opt, opt_id)
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_character_works(
|
||||
self, version: int, release_version: int = None
|
||||
) -> Optional[List[Dict]]:
|
||||
|
||||
conditions = []
|
||||
# default to all works for the given version
|
||||
conditions.append(character_works.c.version == version)
|
||||
if release_version is not None:
|
||||
# limit results to the specified release version
|
||||
conditions.append(character_works.c.releaseVersion == release_version)
|
||||
|
||||
sql = (
|
||||
select(character_works)
|
||||
.where(and_(*conditions))
|
||||
.order_by(character_works.c.sortName)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_gacha(
|
||||
self,
|
||||
version: int,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<tr><td>Nameplate:</td><td style="width: 80%;"><div id="name_nameplate"></div></td></tr>
|
||||
|
||||
<tr><td>Trophy:</td><td><div id="name_trophy">
|
||||
<select name="trophy" id="trophy" onclick="changeTrophy()" style="width:100%;">
|
||||
<select name="trophy" id="trophy" onchange="changeTrophy()" style="width:100%;">
|
||||
{% for item in trophies.values() %}
|
||||
<option value="{{ item["id"] }}" class="trophy-rank{{ item["rarity"] }}">{{ item["name"] }}</option>
|
||||
{% endfor %}
|
||||
@@ -52,7 +52,7 @@
|
||||
</div></td></tr>
|
||||
{% if cur_version >= 17 %} <!-- SubTrophies introduced in VERSE -->
|
||||
<tr><td>Trophy Sub 1:</td><td><div id="name_trophy">
|
||||
<select name="trophy-sub-1" id="trophy-sub-1" onclick="changeTrophySub1()" style="width:100%;">
|
||||
<select name="trophy-sub-1" id="trophy-sub-1" onchange="changeTrophySub1()" style="width:100%;">
|
||||
<option value="-1"></option>
|
||||
{% for item in trophies.values() %}
|
||||
<option value="{{ item["id"] }}" class="trophy-rank{{ item["rarity"] }}">{{ item["name"] }}</option>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div></td></tr>
|
||||
|
||||
<tr><td>Trophy Sub 2:</td><td><div id="name_trophy">
|
||||
<select name="trophy-sub-2" id="trophy-sub-2" onclick="changeTrophySub2()" style="width:100%;">
|
||||
<select name="trophy-sub-2" id="trophy-sub-2" onchange="changeTrophySub2()" style="width:100%;">
|
||||
<option value="-1"></option>
|
||||
{% for item in trophies.values() %}
|
||||
<option value="{{ item["id"] }}" class="trophy-rank{{ item["rarity"] }}">{{ item["name"] }}</option>
|
||||
@@ -88,7 +88,7 @@
|
||||
<button class="collapsible">Nameplate: {{ nameplates|length }}/{{ total_nameplates }}</button>
|
||||
<div id="scrollable-nameplate" class="collapsible-content">
|
||||
{% for item in nameplates.values() %}
|
||||
<img id="nameplate-{{ item["id"] }}" style="padding: 8px 8px;" onclick="changeItem('nameplate', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/nameplate/{{ item["texturePath"] }}" alt="{{ item["name"] }}">
|
||||
<img id="nameplate-{{ item["id"] }}" style="padding: 8px 8px;" onclick="changeItem('nameplate', '{{ item["id"] }}', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/nameplate/{{ item["texturePath"] }}" alt="{{ item["name"] }}">
|
||||
<span id="nameplate-br-{{ loop.index }}"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@
|
||||
<button class="collapsible">Character: {{ characters|length }}/{{ total_characters }}</button>
|
||||
<div id="scrollable-character" class="collapsible-content">
|
||||
{% for item in characters.values() %}
|
||||
<img id="character-{{ item["id"] }}" onclick="changeItem('character', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["iconPath"] }}')" src="img/character/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||
<img id="character-{{ item["illustId"] }}" {{'class="chara-fav" style="background-color:#FF5;"' if item["isFav"] else ""}} onclick="changeItem('character', '{{ item["charaId"] }}', '{{ item["illustId"] }}', '{{ item["name"] }}', '{{ item["iconPath"] }}')" src="img/character/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||
<span id="character-br-{{ loop.index }}"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -123,23 +123,27 @@ document.getElementById("name_nameplate").innerHTML = "Server DB needs upgraded
|
||||
/// This script handles all updates to the user box
|
||||
///
|
||||
total_items = 0;
|
||||
orig_id = 1;
|
||||
orig_name = 2;
|
||||
orig_img = 3;
|
||||
curr_id = 4;
|
||||
curr_name = 5;
|
||||
curr_img = 6;
|
||||
orig_id1 = 1;
|
||||
orig_id2 = 2;
|
||||
orig_name = 3;
|
||||
orig_img = 4;
|
||||
curr_id1 = 5;
|
||||
curr_id2 = 6;
|
||||
curr_name = 7;
|
||||
curr_img = 8;
|
||||
userbox_components = {
|
||||
// [total_items, orig_id, orig_name, orig_img, curr_id, curr_name, curr_img]
|
||||
// [total_items, orig_id1, orig_id2, orig_name, orig_img, curr_id1, curr_id2, curr_name, curr_img]
|
||||
"nameplate":["{{ nameplates|length }}",
|
||||
"{{ profile.nameplateId }}",
|
||||
"{{ profile.nameplateId }}",
|
||||
"{{ nameplates[profile.nameplateId]["name"] }}",
|
||||
"{{ nameplates[profile.nameplateId]["texturePath"] }}", "", "", ""],
|
||||
"{{ nameplates[profile.nameplateId]["texturePath"] }}", "", "", "", ""],
|
||||
|
||||
"character":["{{ characters|length }}",
|
||||
"{{ profile.characterId }}",
|
||||
"{{ characters[profile.characterId]["name"] }}",
|
||||
"{{ characters[profile.characterId]["iconPath"] }}", "", "", ""]
|
||||
"{{ profile.charaIllustId }}",
|
||||
"{{ characters[profile.charaIllustId]["name"] }}",
|
||||
"{{ characters[profile.charaIllustId]["iconPath"] }}", "", "", "", ""]
|
||||
};
|
||||
types = Object.keys(userbox_components);
|
||||
orig_trophy = curr_trophy = "{{ profile.trophyId }}";
|
||||
@@ -153,27 +157,33 @@ function enableButtons(enabled) {
|
||||
document.getElementById("save-btn").disabled = !enabled;
|
||||
}
|
||||
|
||||
function changeItem(type, id, name, img) {
|
||||
function changeItem(type, id1, id2, name, img) {
|
||||
// clear select style for old component
|
||||
var element = document.getElementById(type + "-" + userbox_components[type][curr_id]);
|
||||
var element = document.getElementById(type + "-" + userbox_components[type][curr_id2]);
|
||||
if (element) {
|
||||
element.style.backgroundColor="inherit";
|
||||
if (element.classList.contains("chara-fav")) {
|
||||
element.style.backgroundColor="#FF5";
|
||||
} else {
|
||||
element.style.backgroundColor="inherit";
|
||||
}
|
||||
}
|
||||
|
||||
// set new component
|
||||
userbox_components[type][curr_id] = id;
|
||||
userbox_components[type][curr_id1] = id1;
|
||||
userbox_components[type][curr_id2] = id2;
|
||||
userbox_components[type][curr_name] = name;
|
||||
userbox_components[type][curr_img] = img;
|
||||
|
||||
// update select style for new accessory
|
||||
element = document.getElementById(type + "-" + id);
|
||||
element = document.getElementById(type + "-" + id2);
|
||||
if (element) {
|
||||
element.style.backgroundColor="#5F5";
|
||||
}
|
||||
|
||||
// Update the userbox preview and enable buttons
|
||||
updatePreview();
|
||||
if (id != userbox_components[type][orig_id]) {
|
||||
if (id1 != userbox_components[type][orig_id1] ||
|
||||
id2 != userbox_components[type][orig_id2]) {
|
||||
enableButtons(true);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +235,9 @@ function changeTrophySub2() {
|
||||
|
||||
function resetUserbox() {
|
||||
for (const type of types) {
|
||||
changeItem(type, userbox_components[type][orig_id], userbox_components[type][orig_name], userbox_components[type][orig_img]);
|
||||
changeItem(type,
|
||||
userbox_components[type][orig_id1], userbox_components[type][orig_id2],
|
||||
userbox_components[type][orig_name], userbox_components[type][orig_img]);
|
||||
}
|
||||
// reset trophy
|
||||
document.getElementById("trophy").value = orig_trophy;
|
||||
@@ -246,15 +258,17 @@ function updatePreview() {
|
||||
}
|
||||
|
||||
function saveUserbox() {
|
||||
$.post("/game/chuni/update.userbox", { nameplate: userbox_components["nameplate"][curr_id],
|
||||
$.post("/game/chuni/update.userbox", { nameplate: userbox_components["nameplate"][curr_id2],
|
||||
trophy: curr_trophy,
|
||||
trophySub1: curr_trophy_sub_1,
|
||||
trophySub2: curr_trophy_sub_2,
|
||||
character: userbox_components["character"][curr_id] })
|
||||
character: userbox_components["character"][curr_id1],
|
||||
characterIllust: userbox_components["character"][curr_id2]})
|
||||
.done(function (data) {
|
||||
// set the current as the original and disable buttons
|
||||
for (const type of types) {
|
||||
userbox_components[type][orig_id] = userbox_components[type][curr_id];
|
||||
userbox_components[type][orig_id1] = userbox_components[type][curr_id1];
|
||||
userbox_components[type][orig_id2] = userbox_components[type][curr_id2];
|
||||
userbox_components[type][orig_name] = userbox_components[type][orig_name];
|
||||
userbox_components[type][orig_img] = userbox_components[type][curr_img];
|
||||
}
|
||||
@@ -306,7 +320,7 @@ window.addEventListener('resize', resizePage);
|
||||
resetUserbox();
|
||||
// Initialize scroll on all current items so we can see the selected ones
|
||||
for (const type of types) {
|
||||
document.getElementById("scrollable-" + type).scrollLeft = document.getElementById(type + "-" + userbox_components[type][curr_id]).offsetLeft;
|
||||
document.getElementById("scrollable-" + type).scrollLeft = document.getElementById(type + "-" + userbox_components[type][curr_id2]).offsetLeft;
|
||||
}
|
||||
|
||||
Collapsibles.expandAll();
|
||||
|
||||
Reference in New Issue
Block a user