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:
Hay1tsme
2026-08-12 06:03:07 +00:00
8 changed files with 479 additions and 88 deletions

View File

@@ -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')

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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();