본문으로 이동

미디어위키:Gadget-CU.js

Kawa

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
const MW_BASE_URL = globalThis.MW_BASE_URL || ''

async function fetchJSON(title) {
	const response = await fetch(
		`${MW_BASE_URL}/rest.php/v1/page/${encodeURIComponent(title)}`,
	)
	if (!response.ok) {
		throw new Error(`Fetch failed with ${response.status}`)
	}

	const payload = await response.json()
	payload.source = JSON.parse(payload.source)
	return payload
}

async function fetchCache(title) {
	if (!sessionStorage.getItem(title)) {
		await navigator.locks.request(title, async (lock) => {
			const payload = await fetchJSON(title)
			sessionStorage.setItem(title, JSON.stringify(payload))
		})
	}

	try {
		return JSON.parse(sessionStorage.getItem(title))
	} catch (e) {
		sessionStorage.removeItem(title)
		throw e
	}
}

async function fetchData(ver, lang) {
	const prefix = `CU/data/${ver}`
	const promises = {
		locale: `${prefix}/${lang}.json`,
		items: `${prefix}/items.json`,
		liquids: `${prefix}/liquids.json`,
	}

	return Object.fromEntries(
		await Promise.all(
			Object.entries(promises).map(async ([k, v]) => [k, await fetchCache(v)]),
		),
	)
}

const locale = {
	get: (key, lang = null) => {
		const targetLocale =
			(lang ? locale[lang] : locale[document.documentElement.lang]) || locale.en

		return targetLocale[key] || key
	},

	en: {
		toggleRetroFont: 'Toggle Retro Font',
	},

	ko: {
		toggleRetroFont: '레트로 글꼴 전환',
	},
}

/**
 * @type {Object<string, () => void>}
 */
const scripts = {
	NoRetroFonts: () => {
		const dataKey = 'noRetroFont'
		let dataValue = localStorage.getItem(dataKey) == 'true'

		const apply = () => {
			delete document.documentElement.dataset[dataKey]
			if (dataValue) {
				document.documentElement.dataset[dataKey] = ''
			}
		}

		// 네비게이션 요소
		$('#p-navigation ul').append(
			$('<li id="n-noRetroFont" class="mw-list-item">').append(
				$('<a href="#">')
					.text(locale.get('toggleRetroFont'))
					.on('click', (e) => {
						e.preventDefault()
						dataValue = !dataValue
						localStorage.setItem(dataKey, dataValue)
						apply()
					}),
			),
		)

		apply()
	},

	Tooltip: () => {
		const $body = $('body')
		let $tooltip = null
		let $target = null

		/**
		 * @param {PointerEvent} e
		 * @returns
		 */
		async function onPointerEnter(e) {
			const $currentTarget = $(this)
			const id = $currentTarget.data('cui')
			if (!id || $currentTarget.is($target)) {
				return
			}

			$target = $currentTarget

			const ver = $currentTarget.data('cuiVer') || '6.1'
			const lang = $currentTarget.data('cuiLang') || 'EN'
			const rawLiquids = $currentTarget.data('cuiLiquids')
			const containedLiquids = rawLiquids
				? rawLiquids.split(';').map((v) => v.split(',').map((s) => s.trim()))
				: []

			const { locale, items, liquids } = await fetchData(ver, lang)

			// 타겟 요소가 바뀌었으면 넘어가기
			if (!$currentTarget.is($target)) {
				return
			}

			$target = $currentTarget

			const item = items.source[id]

			let itemValue = item.value
			let itemActuallyUsableOnLimb = item.usableOnLimb

			const headTags = [
				$('<p class="essential">').text(locale.source.main[id] || id),
			]

			const footTags = [
				$('<p class="essential">').text(locale.source.main[id + 'dsc'] || ''),
			]

			const liquidTags = containedLiquids.map(([id, amount]) => {
				const liquid = liquids.source[id]
				if (!liquid) {
					return
				} 

				itemValue += liquid.valuePerLiter * (amount / 1000)
				if (liquid.healthUsable) {
					itemActuallyUsableOnLimb = true
				}

				return $('<section class="cu-liquid">')
					.css(
						'color',
						`color(srgb ${liquid.color.r} ${liquid.color.g} ${liquid.color.b})`,
					)
					.append(
						$('<p class="essential">').text(
							`${locale.source.other[liquid.localeName]} (${amount}ml)`,
						),
						$('<p>').text(locale.source.other[liquid.localeName + 'dsc'] || ''),
					)
			})

			const usableTags = []

			// 액체가 들어있을 수 있는 경우
			if (item.capacity > 0) {
				footTags.push($('<section class="cu-liquids">').append(liquidTags))
			}

			// 무게가 있는 경우
			if (item.weight > 0) {
				footTags.push(
					$('<p class="essential">')
						.append('<i class="cu-icon c-0"></i>')
						.append(`${locale.source.other.weight}: ${item.weight || 0}u`),
				)
			}

			// 손으로만 들 수 있는 경우
			if (item.onlyHoldInHands) {
				footTags.push(
					$('<p style="color:#ff8787">')
						.append('<i class="cu-icon c-6"></i>')
						.append(locale.source.other.itemonlyinhands),
				)
			}

			// 사용할 수 있는 경우
			if (item.usable) {
				usableTags.push(
					$('<span style="color:#a6ffaa">')
						.append('<i class="cu-icon c-6"></i>')
						.append(
							item.usableWithLMB
								? locale.source.other.itemusablehand
								: locale.source.other.itemusableinventory,
						),
				)
			}

			// 사지에 사용할 수 있는 경우
			if (itemActuallyUsableOnLimb) {
				usableTags.push(
					$('<span style="color:#fffb91">')
						.append('<i class="cu-icon c-12"></i>')
						.append(locale.source.other.itemusablewound),
				)
			}

			// 사용할 수 있는 경우
			if (usableTags.length > 0) {
				footTags.push(
					$('<p style="color:#a3a3a3">')
						.append('<i class="cu-icon c-11"></i>')
						.append(locale.source.other.itemusable)
						.append(
							usableTags.reduce(
								(p, c) => (p.length > 0 ? [...p, '/', c] : [c]),
								[],
							),
						),
				)
			}

			// 가치가 있는 경우
			if (itemValue > 0) {
				const displayItemValue = Math.round(Math.min(itemValue, 50))
				footTags.push(
					$('<p style="color:#f6ff73">')
						.append('<i class="cu-icon c-9"></i>')
						.append(`${locale.source.other.itemvalue}${displayItemValue}`),
				)
			}

			$tooltip = $('<div class="cu-tooltip"></div>')
				.append($('<section>').append(headTags))
				.append('<br>')
				.append($('<section>').append(footTags))

			if (e.shiftKey) {
				$tooltip.attr('open', '')
			}

			$body.append($tooltip)
		}

		/**
		 * @param {PointerEvent} e
		 * @returns
		 */
		function onPointerLeave(e) {
			if ($tooltip) {
				$tooltip.remove()
				$tooltip = null
				$target = null
			}
			$target = null
		}

		/**
		 * @param {PointerEvent} e
		 * @returns
		 */
		function onPointerMove(e) {
			if (!$tooltip || !$target) {
				return
			}

			const event = e.type.includes('touch') ? e.originalEvent.touches[0] : e
			const gap = parseInt($tooltip.data('cuiGap'), 10) || 14
			const x = event.clientX + gap
			const y = event.clientY + gap

			requestAnimationFrame(() => {
				if ($tooltip) {
					$tooltip.css({
						left: `${Math.max(0, Math.min(x, window.innerWidth - $tooltip.outerWidth(true)))}px`,
						top: `${Math.max(0, Math.min(y, window.innerHeight - $tooltip.outerHeight(true)))}px`,
					})
				}
			})
		}

		/**
		 * @param {KeyboardEvent} e
		 * @returns
		 */
		function onKey(e) {
			if ($tooltip) {
				$tooltip.removeAttr('open')
				if (e.shiftKey) {
					$tooltip.attr('open', '')
				}
			}
		}

		$('[data-cui]')
			.on('pointerenter', onPointerEnter)
			.on('pointerleave', onPointerLeave)
			.on('pointermove', onPointerMove)
		$('body').on('keyup keydown', onKey)
	},
}

function setup() {
	$('html').addClass('gadget-cu')

	for (const [name, func] of Object.entries(scripts)) {
		try {
			func(name)
		} catch (e) {
			console.error(name, e)
		}
	}
}

if (globalThis.mw) {
	mw.loader.using(['mediawiki.util', 'jquery'], () => {
		$(() => {
			if (mw.config.get('wgCategories').includes('Casualties: Unknown')) {
				setup()
			}
		})
	})
} else {
	setup()
}