(function () {
var VALID_TABS = ['cp', 'brother', 'sister'];
var PARTS_BASE = './assets/parts/';
var PART_NAMES = [
'bg',
'avatar',
'heart',
'button',
'xp-track',
'xp-fill',
'level-pill',
];
var LEVEL_PARTS = {};
for (var partLevel = 1; partLevel <= 5; partLevel += 1) {
LEVEL_PARTS[partLevel] = {};
PART_NAMES.forEach(function (partName) {
LEVEL_PARTS[partLevel][partName] =
PARTS_BASE + 'cp-lv' + partLevel + '-' + partName + '@2x.png';
});
}
var page = document.getElementById('page');
var contentPanel = document.getElementById('contentPanel');
var backButton = document.getElementById('backButton');
var tabButtons = Array.prototype.slice.call(
document.querySelectorAll('.tab-button[data-tab]')
);
var state = {
activeTab: normalizeTab(readParam('tab')) || 'cp',
requestSeq: 0,
cancelingId: '',
tabs: {
cp: createTabState(),
brother: createTabState(),
sister: createTabState(),
},
};
function createTabState() {
return {
loading: false,
loaded: false,
error: '',
relationships: [],
};
}
function readParam(name) {
var params = new URLSearchParams(window.location.search || '');
return params.get(name);
}
function normalizeTab(value) {
var tab = String(value || '')
.trim()
.toLowerCase();
return VALID_TABS.indexOf(tab) >= 0 ? tab : '';
}
function normalizeLevel(value) {
var level = Number(value);
return Number.isFinite(level) && level >= 1 && level <= 5
? Math.floor(level)
: 1;
}
function readNumber(value) {
var number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function readString(value) {
return String(value == null ? '' : value).trim();
}
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function showToast(message) {
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
return;
}
window.dispatchEvent(
new CustomEvent('hyapp:toast', { detail: { message: message } })
);
}
function profileFromData(data) {
var json = data || {};
return {
userId: readString(json.user_id || json.id),
displayUserId: readString(json.display_user_id),
username: readString(json.username || json.nickname || json.name),
avatar: readString(
json.avatar ||
json.avatar_url ||
json.avatar_path ||
(json.profile && json.profile.avatar)
),
};
}
function displayName(profile) {
return (
readString(profile.username) ||
readString(profile.displayUserId) ||
readString(profile.userId) ||
t('cpSpace.unknownUser', 'User')
);
}
function daysFromFormedAt(formedAtMs) {
var formedAt = readNumber(formedAtMs);
if (formedAt <= 0) return 0;
var now = Date.now();
if (now <= formedAt) return 1;
return Math.floor((now - formedAt) / 86400000) + 1;
}
function currentLevelExp(item) {
var total = Math.max(0, readNumber(item.intimacy_value));
var currentThreshold = readNumber(item.current_level_threshold);
if (currentThreshold <= 0 || currentThreshold >= total) return total;
return total - currentThreshold;
}
function nextLevelExp(item) {
if (item.max_level) return 0;
var nextThreshold = readNumber(item.next_level_threshold);
var currentThreshold = readNumber(item.current_level_threshold);
if (nextThreshold > 0) {
if (currentThreshold > 0 && nextThreshold > currentThreshold) {
return nextThreshold - currentThreshold;
}
return nextThreshold;
}
var total = Math.max(0, readNumber(item.intimacy_value));
var remaining = readNumber(item.needed_for_next_level);
return remaining > 0 ? total + remaining : 0;
}
function progressPercent(item) {
if (item.max_level) return 100;
var explicit = readNumber(item.level_progress_percent);
if (explicit > 0) return Math.max(0, Math.min(100, explicit));
var next = nextLevelExp(item);
if (next <= 0) return 0;
return Math.max(0, Math.min(100, (currentLevelExp(item) / next) * 100));
}
function normalizeRelationship(item, tab) {
var relationType = normalizeTab(item && item.relation_type) || tab;
var level = normalizeLevel(item && item.level);
var me = profileFromData(item && item.me);
var partner = profileFromData(item && item.partner);
var currentExp = currentLevelExp(item || {});
var nextExp = nextLevelExp(item || {});
return {
relationshipId: readString(item && item.relationship_id),
relationType: relationType,
status: readString(item && item.status) || 'active',
level: level,
me: me,
partner: partner,
days: daysFromFormedAt(item && item.formed_at_ms),
intimacyValue: Math.max(0, readNumber(item && item.intimacy_value)),
currentExp: currentExp,
nextExp: nextExp,
progressPercent: progressPercent(item || {}),
maxLevel: Boolean(item && item.max_level),
breakupCostCoins: Math.max(
0,
readNumber(item && item.breakup_cost_coins)
),
};
}
function activeRelationships(data, tab) {
var list = Array.isArray(data && data.relationships)
? data.relationships
: [];
return list
.map(function (item) {
return normalizeRelationship(item || {}, tab);
})
.filter(function (item) {
return (
item.relationshipId &&
item.relationType === tab &&
item.status === 'active'
);
});
}
function setActiveTab(tab) {
var nextTab = normalizeTab(tab) || 'cp';
state.activeTab = nextTab;
page.setAttribute('data-tab', nextTab);
tabButtons.forEach(function (button) {
button.classList.toggle(
'is-active',
button.getAttribute('data-tab') === nextTab
);
});
render();
if (!state.tabs[nextTab].loaded && !state.tabs[nextTab].loading) {
loadTab(nextTab);
}
}
function visualLevel(item) {
return item.relationType === 'cp' ? item.level : 1;
}
function partsFor(item) {
return LEVEL_PARTS[visualLevel(item)] || LEVEL_PARTS[1];
}
function nextLevelLabel(item) {
if (item.maxLevel) return 'Max';
return t('cpSpace.levelValue', 'Lv.{level}').replace(
'{level}',
String(Math.min(5, item.level + 1))
);
}
function expLabel(item) {
if (item.maxLevel || item.nextExp <= 0) {
return t('cpSpace.maxLevelExp', '{exp} EXP').replace(
'{exp}',
formatNumber(item.intimacyValue)
);
}
return t('cpSpace.expProgress', '{current}/{next} EXP')
.replace('{current}', formatNumber(item.currentExp))
.replace('{next}', formatNumber(item.nextExp));
}
function formatNumber(value) {
var number = Math.max(0, Math.floor(readNumber(value)));
return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function avatarHTML(profile, side, frameSrc) {
var name = displayName(profile);
var avatar = readString(profile.avatar);
return [
'',
].join('')
: [
'
',
'
',
avatarHTML(item.me, 'left', parts.avatar),
avatarHTML(item.partner, 'right', parts.avatar),
'
',
'
',
escapeHTML(levelTitle),
'
',
escapeHTML(nextLevelLabel(item)),
'