Compare commits
10 Commits
686962b678
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
b2178fec85
|
|||
|
ee32060a7f
|
|||
|
d231ae7c38
|
|||
|
5c83e45d2a
|
|||
|
50c0646a93
|
|||
|
8263dc1c93
|
|||
|
c506bad739
|
|||
|
a0e2915c3d
|
|||
|
c7d0d929be
|
|||
|
8ee981b949
|
@@ -107,4 +107,15 @@ defineProps<{
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 650px) {
|
||||||
|
.item-row {
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
.first-back-content {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ const itemIconPath = computed(() => CDRAGON_BASE + mapPath(props.item.iconPath))
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--color-on-surface);
|
border: 1px solid var(--color-on-surface);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: help;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ refreshStyles()
|
|||||||
<div class="rune-spacer-bar" />
|
<div class="rune-spacer-bar" />
|
||||||
<div class="rune-holder" style="align-content: end">
|
<div class="rune-holder" style="align-content: end">
|
||||||
<div class="rune-slot">
|
<div class="rune-slot">
|
||||||
<img style="margin: auto" :src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)" />
|
<img
|
||||||
|
class="rune-style-img"
|
||||||
|
style="margin: auto"
|
||||||
|
:src="CDRAGON_BASE + mapPath(secondaryStyle.iconPath)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(slot, slotIndex) in secondaryStyle.slots.slice(1, 4)"
|
v-for="(slot, slotIndex) in secondaryStyle.slots.slice(1, 4)"
|
||||||
@@ -128,6 +132,10 @@ refreshStyles()
|
|||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
border: 1px var(--color-on-surface) solid;
|
border: 1px var(--color-on-surface) solid;
|
||||||
}
|
}
|
||||||
|
.rune-style-img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 650px) {
|
@media only screen and (max-width: 650px) {
|
||||||
.rune-slot {
|
.rune-slot {
|
||||||
@@ -143,5 +151,9 @@ refreshStyles()
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
.rune-icon {
|
||||||
|
width: 24px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ const perkIconPath = computed(() => CDRAGON_BASE + mapPath(props.perk.iconPath))
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--color-on-surface);
|
border: 1px solid var(--color-on-surface);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: help;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ const readFileAsync = promisify(readFile)
|
|||||||
const CDRAGON_BASE = 'https://raw.communitydragon.org/'
|
const CDRAGON_BASE = 'https://raw.communitydragon.org/'
|
||||||
|
|
||||||
// Cache directory - can be configured via environment variable
|
// Cache directory - can be configured via environment variable
|
||||||
// Default to dev/cdragon for development
|
// In development, use dev/data/cdragon relative to project root
|
||||||
|
// In production, use /cdragon (shared volume)
|
||||||
const getCacheDir = () => {
|
const getCacheDir = () => {
|
||||||
if (process.env.CDRAGON_CACHE_DIR) {
|
if (process.env.CDRAGON_CACHE_DIR) {
|
||||||
return process.env.CDRAGON_CACHE_DIR
|
return process.env.CDRAGON_CACHE_DIR
|
||||||
}
|
}
|
||||||
// Default to dev/cdragon relative to project root
|
// Check if we're in development mode (explicitly set)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return join(process.cwd(), '..', 'dev', 'data', 'cdragon')
|
return join(process.cwd(), '..', 'dev', 'data', 'cdragon')
|
||||||
|
}
|
||||||
|
// Default to /cdragon for production (Docker)
|
||||||
|
return '/cdragon'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ RUN npm install
|
|||||||
COPY --chown=node:node match_collector/. .
|
COPY --chown=node:node match_collector/. .
|
||||||
|
|
||||||
FROM node:current-alpine
|
FROM node:current-alpine
|
||||||
|
# Install su-exec for dropping privileges
|
||||||
|
RUN apk add --no-cache su-exec
|
||||||
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
|
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
USER node
|
|
||||||
COPY --from=build --chown=node:node /home/node/app/match_collector/node_modules ./node_modules
|
COPY --from=build --chown=node:node /home/node/app/match_collector/node_modules ./node_modules
|
||||||
COPY --from=build --chown=node:node /home/node/app/match_collector/. .
|
COPY --from=build --chown=node:node /home/node/app/match_collector/. .
|
||||||
CMD ["/bin/sh", "-c", "node --import=tsx src/index.ts; sleep 20h"]
|
COPY --chown=node:node match_collector/docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
# Run entrypoint as root to fix permissions, then drop to node user
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
CMD ["/bin/sh", "-c", "node --import=tsx src/index.ts; sleep 12h"]
|
||||||
|
|||||||
9
match_collector/docker-entrypoint.sh
Normal file
9
match_collector/docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Fix permissions on the cdragon cache directory if it exists
|
||||||
|
if [ -d "/cdragon" ]; then
|
||||||
|
# Ensure the node user owns the cdragon directory
|
||||||
|
chown -R node:node /cdragon 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute the main command as the node user
|
||||||
|
exec su-exec node "$@"
|
||||||
@@ -46,7 +46,8 @@ async function downloadCDragonAssets(patch: string) {
|
|||||||
console.log(`\n=== Downloading CDragon assets for patch ${cdragonPatch} ===`)
|
console.log(`\n=== Downloading CDragon assets for patch ${cdragonPatch} ===`)
|
||||||
|
|
||||||
// Get cache directory from environment or use default
|
// Get cache directory from environment or use default
|
||||||
// In development, use a local directory relative to project root; in production (Docker), use /cdragon
|
// In development, use a local directory relative to project root
|
||||||
|
// In production (Docker), use /cdragon (shared volume with frontend)
|
||||||
const defaultCacheDir =
|
const defaultCacheDir =
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? resolve(__dirname, '../../dev/data/cdragon')
|
? resolve(__dirname, '../../dev/data/cdragon')
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function handleMatchBuilds(
|
|||||||
participantIndex: number,
|
participantIndex: number,
|
||||||
builds: Builds,
|
builds: Builds,
|
||||||
platform?: string
|
platform?: string
|
||||||
): Build {
|
): { build: Build; startItemId: number | undefined } {
|
||||||
const timeline: Timeline = match.timeline
|
const timeline: Timeline = match.timeline
|
||||||
|
|
||||||
// Find or create the build for this participant's rune configuration
|
// Find or create the build for this participant's rune configuration
|
||||||
@@ -185,6 +185,7 @@ function handleMatchBuilds(
|
|||||||
build.count += 1
|
build.count += 1
|
||||||
|
|
||||||
const items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }> = []
|
const items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }> = []
|
||||||
|
let startItemId: number | undefined = undefined
|
||||||
for (const frame of timeline.info.frames) {
|
for (const frame of timeline.info.frames) {
|
||||||
for (const event of frame.events) {
|
for (const event of frame.events) {
|
||||||
if (event.participantId != participantIndex) continue
|
if (event.participantId != participantIndex) continue
|
||||||
@@ -218,17 +219,10 @@ function handleMatchBuilds(
|
|||||||
}
|
}
|
||||||
if (event.type != 'ITEM_PURCHASED') continue
|
if (event.type != 'ITEM_PURCHASED') continue
|
||||||
|
|
||||||
// Handle boots upgrades
|
|
||||||
if (
|
|
||||||
itemInfo.requiredBuffCurrencyName == 'Feats_NoxianBootPurchaseBuff' ||
|
|
||||||
itemInfo.requiredBuffCurrencyName == 'Feats_SpecialQuestBootBuff'
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle boots differently
|
// Handle boots differently
|
||||||
if (itemInfo.categories.includes('Boots')) {
|
if (itemInfo.categories.includes('Boots')) {
|
||||||
if (itemInfo.to.length == 0 || (itemInfo.to[0] >= 3171 && itemInfo.to[0] <= 3176)) {
|
// Ignore basic boots, only count Tier 2 boots
|
||||||
|
if (event.itemId != 1001) {
|
||||||
// Check for bootsFirst
|
// Check for bootsFirst
|
||||||
if (items.length < 2) {
|
if (items.length < 2) {
|
||||||
build.bootsFirstCount += 1
|
build.bootsFirstCount += 1
|
||||||
@@ -267,9 +261,11 @@ function handleMatchBuilds(
|
|||||||
// This tree includes start item as the root, then branching paths
|
// This tree includes start item as the root, then branching paths
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
treeMerge(build.items, items)
|
treeMerge(build.items, items)
|
||||||
|
// The first item is the starter item
|
||||||
|
startItemId = items[0].itemId
|
||||||
}
|
}
|
||||||
|
|
||||||
return build
|
return { build, startItemId }
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMatch(match: Match, champions: Map<number, ChampionData>, platform?: string) {
|
function handleMatch(match: Match, champions: Map<number, ChampionData>, platform?: string) {
|
||||||
@@ -365,14 +361,22 @@ function handleMatch(match: Match, champions: Map<number, ChampionData>, platfor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Items and runes (builds)
|
// Items and runes (builds)
|
||||||
const build = handleMatchBuilds(match, participant, participantIndex, lane.builds, platform)
|
const { build, startItemId } = handleMatchBuilds(
|
||||||
|
match,
|
||||||
|
participant,
|
||||||
|
participantIndex,
|
||||||
|
lane.builds,
|
||||||
|
platform
|
||||||
|
)
|
||||||
|
|
||||||
// First back data - store at build level
|
// First back data - store at build level with start item tracking
|
||||||
const firstBackData = extractFirstBackFromMatch(match, participantIndex)
|
const firstBackData = extractFirstBackFromMatch(match, participantIndex)
|
||||||
if (firstBackData) {
|
if (firstBackData) {
|
||||||
if (!build.firstBacksRaw) {
|
if (!build.firstBacksRaw) {
|
||||||
build.firstBacksRaw = []
|
build.firstBacksRaw = []
|
||||||
}
|
}
|
||||||
|
// Include the starter item ID for proper filtering when splitting builds
|
||||||
|
firstBackData.startItemId = startItemId
|
||||||
build.firstBacksRaw.push(firstBackData)
|
build.firstBacksRaw.push(firstBackData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,16 +449,44 @@ function splitMergeOnStarterItem(build: Build, championName: string): BuildWithS
|
|||||||
console.log(`Warning: for champion ${championName}, start item splits build variant.`)
|
console.log(`Warning: for champion ${championName}, start item splits build variant.`)
|
||||||
const builds = []
|
const builds = []
|
||||||
for (const c of build.items.children) {
|
for (const c of build.items.children) {
|
||||||
|
// Calculate the ratio for proportional distribution
|
||||||
|
const ratio = c.count / build.count
|
||||||
|
|
||||||
|
// Proportionally distribute boots counts
|
||||||
|
const scaledBoots = build.boots.map(b => ({
|
||||||
|
data: b.data,
|
||||||
|
count: Math.round(b.count * ratio)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Proportionally distribute suppItems counts
|
||||||
|
const scaledSuppItems = build.suppItems.map(s => ({
|
||||||
|
data: s.data,
|
||||||
|
count: Math.round(s.count * ratio)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Proportionally distribute bootsFirstCount
|
||||||
|
const scaledBootsFirstCount = Math.round(build.bootsFirstCount * ratio)
|
||||||
|
|
||||||
|
// Filter firstBacksRaw by starter item
|
||||||
|
let filteredFirstBacksRaw: FirstBackData[] | undefined
|
||||||
|
if (build.firstBacksRaw && build.firstBacksRaw.length > 0) {
|
||||||
|
// Filter by the starter item ID that was tracked when storing firstBacksRaw
|
||||||
|
filteredFirstBacksRaw = build.firstBacksRaw.filter(fb => fb.startItemId === c.data)
|
||||||
|
if (filteredFirstBacksRaw.length === 0) {
|
||||||
|
filteredFirstBacksRaw = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builds.push({
|
builds.push({
|
||||||
runeKeystone: build.runeKeystone,
|
runeKeystone: build.runeKeystone,
|
||||||
runes: build.runes,
|
runes: build.runes,
|
||||||
items: c,
|
items: c,
|
||||||
bootsFirstCount: build.bootsFirstCount,
|
bootsFirstCount: scaledBootsFirstCount,
|
||||||
count: c.count,
|
count: c.count,
|
||||||
startItems: [{ data: c.data!, count: c.count }],
|
startItems: [{ data: c.data!, count: c.count }],
|
||||||
suppItems: build.suppItems,
|
suppItems: scaledSuppItems,
|
||||||
boots: build.boots,
|
boots: scaledBoots,
|
||||||
firstBacksRaw: build.firstBacksRaw
|
firstBacksRaw: filteredFirstBacksRaw
|
||||||
})
|
})
|
||||||
c.data = undefined
|
c.data = undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,12 +264,8 @@ function deriveTags(node: ItemTree, expectedRegionDistribution?: PlatformCounts)
|
|||||||
const totalExpected = REGION_KEYS.reduce((sum, key) => sum + expectedRegionDistribution[key], 0)
|
const totalExpected = REGION_KEYS.reduce((sum, key) => sum + expectedRegionDistribution[key], 0)
|
||||||
|
|
||||||
if (totalExpected > 0) {
|
if (totalExpected > 0) {
|
||||||
// Tag if the item is significantly more popular in a region (>= 1.5x expected rate)
|
// Tag if one region accounts for >= 60% of the normalized distribution
|
||||||
// and has a minimum absolute percentage (>= 10%)
|
// Normalized value = actual percentage / expected percentage ratio
|
||||||
const SIGNIFICANCE_THRESHOLD = 1.5
|
|
||||||
const MINIMUM_PCT = 0.1
|
|
||||||
|
|
||||||
// Loop through all regions to derive tags
|
|
||||||
const regionTags: Array<{ key: keyof PlatformCounts; tag: ItemTag }> = [
|
const regionTags: Array<{ key: keyof PlatformCounts; tag: ItemTag }> = [
|
||||||
{ key: 'euw', tag: 'region_euw' },
|
{ key: 'euw', tag: 'region_euw' },
|
||||||
{ key: 'eun', tag: 'region_eun' },
|
{ key: 'eun', tag: 'region_eun' },
|
||||||
@@ -277,12 +273,23 @@ function deriveTags(node: ItemTree, expectedRegionDistribution?: PlatformCounts)
|
|||||||
{ key: 'kr', tag: 'region_kr' }
|
{ key: 'kr', tag: 'region_kr' }
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const { key, tag } of regionTags) {
|
// Calculate normalized values (actual/expected ratio) for each region
|
||||||
|
const normalizedValues = regionTags.map(({ key, tag }) => {
|
||||||
const expectedPct = expectedRegionDistribution[key] / totalExpected
|
const expectedPct = expectedRegionDistribution[key] / totalExpected
|
||||||
const actualPct = node.platformCount[key] / totalRegionCount
|
const actualPct = node.platformCount[key] / totalRegionCount
|
||||||
|
const normalizedValue = expectedPct > 0 ? actualPct / expectedPct : 0
|
||||||
|
return { tag, value: normalizedValue }
|
||||||
|
})
|
||||||
|
|
||||||
if (actualPct >= expectedPct * SIGNIFICANCE_THRESHOLD && actualPct >= MINIMUM_PCT) {
|
const totalNormalized = normalizedValues.reduce((sum, { value }) => sum + value, 0)
|
||||||
|
|
||||||
|
// Tag the region if it accounts for >= 60% of the normalized distribution
|
||||||
|
if (totalNormalized > 0) {
|
||||||
|
for (const { tag, value } of normalizedValues) {
|
||||||
|
if (value / totalNormalized >= 0.6) {
|
||||||
tags.push(tag)
|
tags.push(tag)
|
||||||
|
break // Only tag the most dominant region
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export interface ItemSet {
|
|||||||
export interface FirstBackData {
|
export interface FirstBackData {
|
||||||
timestamp: number
|
timestamp: number
|
||||||
itemSet: ItemSet
|
itemSet: ItemSet
|
||||||
|
startItemId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user