Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證DD
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
브라우저에서 바로 돌아가는 5대5 전술 FPS. 오리지널 요원 7명, 무기 20종, 맵 10종. 외부 이미지·사운드 파일이 하나도 없습니다. 텍스처·초상화·효과음을 전부 코드로 만듭니다. 1. GitHub에서 새 저장소를 만듭니다 (Public으로). 2. 이 폴더의 파일 4개를 올립니다. 3. https://vercel.com 에서 Add New → Project → 저장소 선택 → Deploy. 4. 끝. 주소가 나오면 친구에게 보내면 됩니다. Vercel 설정은 건드릴 필요 없습니다. 정적 사이트로 자동 인식됩니다. play.html은 더블클릭해도 바로 실행됩니다. 다만 친구와 하려면 웹에 올려야 합니다. 초기 화면에서 친구와 하기를 누르면 6자리 방 코드가 나옵니다. 친구는 코드로 참가에 그 코드를 넣으면 됩니다.檢視完整 README ↗
브라우저에서 바로 하는 5대5 공격/수비 전술 FPS입니다. 공격팀은 스파이크를 설치하고, 수비팀은 막거나 해제합니다.
js/engine/agents.js이며 js/legacy/의 여러 묶음이 보완합니다.js/legacy/107-twenty-eight-agents-…js, window.ROSTER_CUT).js/ability-motions.js, npm run test:ability-motions).finisherSkin·byBot로 구분합니다 (js/game-rules.js, js/game-events.js, npm run test:finisher-always).js/legacy/58-gun-skins.js의 botSkins).js/legacy/03-engine.js의 handleSpike, npm run test:manual-plant).api/v1.js의 RESERVED_ID, guestName).admin.html (/admin).manifest.webmanifest, sw.js. Vercel에 배포됩니다.효과음, 초상화, 일부 텍스처는 코드로 만듭니다. 맵 표면 재질과 하늘 조명은 파일을 씁니다.
assets/mat/: 표면 재질 webp 54개 (색, 노멀, ORM). play.html이 assets/mat/<재질>_<종류>.webp로 불러옵니다.assets/env/: 하늘 조명 HDR 7개 (alpine, city, dawn, forest, park, studio, warehouse). assets/env/<이름>.hdr로 불러옵니다.assets/CREDITS.md에 있습니다 (CC0).js/legacy/61-kill-banners.js). 소리와 모양만 바뀌고 전투에는 영향이 없습니다.js/legacy/63-heavier-gunshots.js).js/legacy/76-*, 77-*).랭크전 찾기로 열린 랭크 방에 들어가거나 새로 엽니다. 랭크 방에서는 관리자 설정, 치트, 계정별 치트 권한, 운영자 방 명령, 방장의 규칙 변경이 모두 꺼지고 모드·맵·라운드는 방을 열 때 정해진 대로 갑니다. 서버는 랭크 방에서 시작한 경기의 확인표에만 랭크 표시를 서명하고, 사람이 rank.minHumans명 이상일 때만 RR을 바꿉니다. 일반 경기는 XP와 골드만 줍니다 (js/legacy/81-ranked-*.js, api/v1.js의 match_start·progress·room_up).js/legacy/82-shift-arena-*.js).js/legacy/54-reload-animation.js).js/legacy/52-projectiles.js).js/legacy/83-knife-tricks-*.js).상점 탭. 매일 자정(한국 시간)에 스킨 20개(일반 7·희귀 6·영웅 4·전설 3)가 바뀌고 모두에게 같은 목록이 뜹니다. 뽑기와 달리 원하는 스킨을 골라 골드로 바로 삽니다. 가격은 뽑기보다 비싸게 정했습니다: 일반 1,200 · 희귀 3,000 · 영웅 7,000 · 전설 15,000 골드, 칼 모양 스킨은 1.5배 (뽑기 1회 300). 구매 버튼은 두 번 눌러야 사지고(첫 번째는 확인), 골드가 모자라면 얼마가 부족한지 보여 줍니다. 목록은 서버가 날짜와 비밀키로 정하므로 내일 목록을 미리 알 수 없고, 살 때 서버가 오늘 목록을 다시 계산해 확인합니다 (shop, shop_buy). 스킨을 얻는 길은 뽑기와 상점 두 가지입니다. 운영자는 '게임 설정 → 일일 상점'에서 열기·닫기, 등급별 개수, 가격, 칼 배율을 정하고, 대시보드에서 오늘 목록을 보고 상점 새로 고침으로 바로 바꿉니다. 구매는 서버 로그(상점)와 대시보드(상점 구매 수·쓴 골드)에 남습니다.봇의 세기는 skill 숫자 하나로 정합니다 (js/bot-skill.js, 순수 함수라 tests/bot-skill.test.js가 직접 읽습니다). skill은 예전 보통 봇의 배수로, 1.00이 정확히 예전 보통입니다. 1.00 아래에서는 회전 속도·사거리·피해·헤드샷은 곱하고 조준 오차·탄 퍼짐은 나눕니다 (0.60이면 보통의 60%). 1.00 위에서는 보통에서 예전 빡셈까지 직선으로 올라 1.20이 빡셈과 같고, 그 위는 잘라내므로 에임봇은 없습니다.
room.diff의 easy는 0.60, hard는 1.20으로 고정되고 normal(기본값, 경쟁방 포함)이 위의 자동 값을 따릅니다.BOT_RT.skill에 두고, diffTune()(js/legacy/03-engine.js)과 탄 퍼짐(모듈 31)과 피해 배율이 그 숫자만 읽습니다.점수판·로비 슬롯·킬 피드의 이름 옆에 Lv N 칩이 붙습니다 (js/legacy/109-*). 게스트의 레벨·티어는 hello 메시지로 방장에게 가고, 방장이 로비·참가자 목록·ctrl 이벤트로 나머지에게 전합니다. 게스트가 hello에 적은 레벨·티어는 주장일 뿐입니다. 방장이 서버(api/v1.js의 공개 액션 levels, ids 12개까지, 주소당 분당 120회)에 이름마다 실제 계정의 레벨·티어를 물어보고, 서버 값이 주장을 대신해 방의 봇 세기와 칩에 쓰입니다 (낮은 레벨을 적어 쉬운 봇을 받을 수 없습니다). 응답에는 레벨과 공개 티어(배치 중이면 숨김)만 있고 MMR·이메일·토큰·IP·기기는 없으며, 게스트·없는 이름·잘못된 이름은 모두 같은 빈 값입니다. 랭크 방은 ranked_start의 응답(lv)이 같은 서버 값을 한 번 더 줍니다. 서버에 닿지 못하면 주장은 예전처럼 봇 세기에만 쓰이고 칩은 보이지 않습니다 (서버가 준 숫자만 칩에 나옵니다). 이름만 보고 계정을 찾으므로, 남의 계정 이름을 대는 것까지는 막지 못합니다. 계정이 없는 게스트와 봇에는 칩이 없습니다. 검사는 npm test(표)와 npm run test:bot-skill(= test:verified-levels, 실제 게임 두 창: 주장 Lv 1 대 실제 Lv 30, 서버가 안 될 때)입니다.
npm test는 단위 테스트(tests/)입니다. 브라우저 검사(npm run test:browser, test:regression, test:startup, test:maps, test:online, test:ranked, test:shift, test:reload, test:knife, test:aim, test:admin, test:shop, test:modes2, test:wish, test:train, test:agents4, test:season, test:infection, test:conquest, test:range, test:cosmetics, test:newmaps, test:hospital, test:hospital-bots, test:landing, test:gunshots, test:details, test:ui-fixes, test:ctf, test:zone, test:feedback, test:orders, test:followup)는 Playwright가 필요하고 DFDF_PLAYWRIGHT_MODULE, DFDF_BASE_URL 등을 설정합니다. 소프트웨어 GL(헤드리스) 환경에서는 느려서 test:regression의 첫 클릭과 test:followup의 실제 클릭이 간헐적으로 시간 초과됩니다. 코드를 바꾸지 않은 브랜치에서도 같았습니다.
| 파일 | 설명 |
|---|---|
play.html | 게임 화면·스타일과 스크립트 실행 순서 |
js/legacy/ | 기존 엔진·요원·맵·계정·스킨 기능 103개, 순서는 manifest.json |
js/engine/ | 초기 요원·무기·맵 데이터, 텍스처·초상화 생성 |
js/ | 공통 요청·오디오·처치 원인·홈·포커스·부팅 관리 |
index.html | 소개 페이지 |
admin.html | 관리자 페이지 |
api/v1.js | 서버 함수 (?a=<action>), 데이터는 Upstash Redis REST |
sw.js, manifest.webmanifest | PWA |
vercel.json | 배포 설정 |
RR and the hidden MMR are computed only in api/v1.js from a match the host attested; what a player reports about himself (humans, win) is never used for ranked RR. All calls are POST /api/v1?a=<action> with a JSON body.
ranked_start {token, room, a: [ids], b: [ids]} (host, ranked room) returns {id, t0, rm, a, b, ownerPlayed}. rm is a signed ticket for the host. Unknown, guest and banned accounts are dropped from the roster; the host must stay in it. MMR of everyone is snapshotted here.match_start {token, ranked: true, room} (every player) returns {ticket, ranked}; the ticket (mt) carries the match id. 403 if the account is not in the roster, 409 until the host has called ranked_start. Asking again returns the same ticket.ranked_result {token, rm, w: "a"|"b"|"draw", k: {id: kills}} (host, after at least 60 s) returns {accepted, duplicate, w}. The first result stands.progress {token, mt, kills, win, outcome, match, giveUp?} (every player, unchanged for normal matches). In a ranked match win/outcome must equal the attested result for the player's team (else 409, counted as a dissent); humans is ignored. 425 means the host has not reported yet (retry); giveUp: true takes the plain XP/gold reward without RR. The answer adds rankedCounted, rankedWhy (few_humans, draw, disputed, no_result, duplicate), placement: {done, of: 5} and rr (the change; 0 while placing).Rules: at least rank.minHumans roster accounts must have fetched a ticket; a draw, a match under 60 s and a disputed match move nothing; a match is disputed when a strict majority of the non-host players of a team dissent; an account settles a match once; rank.maxPerDay still caps daily RR gain. The operator's matches count like anyone's (ownerPlayed: true is kept on the match record); cheats are closed in ranked rooms for everyone but the operator (owner_ticket is issued to the owner only, perm_ticket never).
MMR (Elo, field mmr, placed): older accounts get 1000 + RR on first read (placed if they have RR or ranked history). The first 5 counted matches are placement (K = 120, tier hidden: user.rank.hidden === true, user.placement); after the fifth, RR = MMR - 1000. After that RR change = base win/loss x clamp(0.5 + (1 - expected) | 0.5 + expected, 0.5, 1.5) + the kill bonus, with expected from the two teams' average MMR (pure helpers in module.exports.rankMath). A season roll shrinks MMR toward 1000 by the same season.keep factor as RR.
leaderboard {n?} (public, default 50, max 100) returns {season: {n, end}, rows: [{pos, name, tier, t, div, rr, wins, played}]}: placed accounts that counted a ranked match this season (Redis sorted set lb:<season>), no MMR or private data.
Matchmaking: rooms and room_up give a ranked room band (its average MMR rounded to 50, null when unknown) and range (150 MMR, +50 per minute the room has been open, up to 600). ranked_can_join {token, code} returns {ranked, ok, range, band}; the operator and rooms without a measured average are always open.
The client side of the flow above (js/legacy/109-ranked-flow-*.js, with 44-account-sync.js, 71-…rank…, 81-ranked-play-… and js/leaderboard.js). Ranked rooms are for signed-in accounts only; a guest is refused before joining (and sent out if it got in by a link).
ranked_start and tells the room (rk-go); everyone else waits for that, then calls match_start {ranked: true, room} (retrying on 409; on 403 or any failure the match gets a plain ticket: XP and gold, no RR). When the match is over the host sends ranked_result (winning side, kills per account), then every player's progress report settles it. The report queue keeps a ranked report on 425 (the host's result is not in yet) and asks again every few seconds; after two minutes it asks with giveUp: true (plain reward); a 409 drops it. humans is no longer sent.user.placement.done < 5 the title chip, the result screen and the "Play together" card show "배치 n/5" with a "?" gem instead of a tier; the fifth counted match reveals the tier (result screen cell and banner). A counted match afterwards shows +/-RR; a match that did not count says why (few_humans, draw, disputed, no_result).band as the tier it sits in. Joining a ranked room (list, code, link, "Find a ranked match") first asks ranked_can_join; out of range gives a Korean message (the room list note and a banner). Note the server answers an out-of-range room with HTTP 200 {ok: false, ranked, range, band}, so the client reads that answer raw instead of through the shared api() (which treats ok: false as an error).play.html#ranks, linked from the homepage): the season, the top 50 with tier, RR and wins, my row marked.owner_ticket for that room (a normal account gets none, so editing window.IS_OWNER opens nothing). A confirmed operator keeps his own cheats (the host listens to his personal switches and to his commands about himself); room-wide switches (speed, gravity, round time, bots, …), cheats for others and kick/move/message/close stay shut for him too. His match counts for RR like anyone's (the server marks ownerPlayed).npm test (tests/ranked-client.test.js), npm run test:ranked (the room and its lock), (three browsers: roster, tickets, result, settle, placement, reveal, leaderboard screen, band and range message, cheat lock for a normal account and for the operator), .| 이름 | 용도 |
|---|---|
KV_REST_API_URL / UPSTASH_REDIS_REST_URL | Redis 주소 |
KV_REST_API_TOKEN / UPSTASH_REDIS_REST_TOKEN | Redis 토큰 |
ADMIN_MASTER_PASS | 관리자 비밀번호 |
SESSION_SECRET | 세션·매치 티켓 서명 키 |
OWNER_ID | 마스터 비밀번호로 로그인하는 소유자 계정 아이디 |
CF_TURN_KEY_ID, CF_TURN_API_TOKEN | Cloudflare TURN 임시 접속 정보 발급 |
TURN_URLS, TURN_USERNAME, TURN_CREDENTIAL | 대체 TURN 서버 접속 설정 |
SESSION_SECRET이 없으면 관리자 비밀번호와 Redis 토큰에서 만든 값을 대신 씁니다. 그러면 비밀번호를 바꿀 때 기존 티켓이 모두 무효가 되므로, Vercel에 따로 설정해 두세요.
Node.js 20 이상에서 npm run dev로 정적 페이지와 /api/v1을 실행합니다. 실제 계정 기능에는 위 Redis 환경변수가 필요합니다. 환경변수가 없으면 저장 요청이 실패하며 운영 데이터를 흉내 내지 않습니다.
설정 → 그래픽은 휴대폰·성능·비주얼·최고 네 모드입니다. 새 휴대폰 사용자는 휴대폰, PC 사용자는 성능 모드로 시작합니다. 휴대폰·성능은 그림자·후처리·MSAA·탄피를 끄고 화면 해상도와 장식 이펙트를 줄입니다. 비주얼·최고는 상세 총기·그림자·조명·후처리를 켭니다. 동적 해상도는 선택한 모드 안에서만 조절하며 다른 모드로 자동 변경하지 않습니다. 이전 최소/low/fast는 휴대폰, 균형은 성능, 자동은 기기 기본값으로 이전합니다. 제공된 원본 무기·스킨·마크X발로 지형 파일은 보존합니다.
npm run check: 서버, 기능 모듈 및 HTML의 인라인 스크립트 문법 검사.npm test: 임시 메모리 저장소로 인증 경계, 동시 저장, 보상 재시도, 입력·능력·피니셔 조건 검사. 운영 Redis의 통합 검증은 별도로 필요합니다.npm run test:browser: 실행 중인 개발 서버에서 작은 가로 화면의 홈·요원 선택·무기고·처치·결과·로그아웃을 검사합니다. Playwright와 Chromium이 필요합니다. 선택적으로 DFDF_BASE_URL, CHROME_BIN, DFDF_PLAYWRIGHT_MODULE, DFDF_THREE_PATH를 설정할 수 있습니다.npm run test:maps: 문·순간이동 장치·집라인의 맵 재로딩 후 자원 해제, 중복 해제 방지, 공유 자산 유지, 문 조작 및 반복 렌더링 검사.npm run test:performance: 일반 맵 선택으로 마크X발로를 실행해 미니맵 지형 캐시 재사용, 원래 지형 그림과 픽셀 일치, 실시간 스파이크 표시를 검사합니다. 120회 게임 처리 시간과 그리기 횟수를 test-results/performance.json에 기록합니다. 헤드리스 CPU 처리 시간이며 실제 기기 FPS는 아닙니다. 수정 전 엔진·폐병원 스크립트 파일을 DFDF_PERF_BASELINE_ENGINE, DFDF_PERF_BASELINE_HOSPITAL로 지정하면 같은 실행에서 전후를 비교합니다.npm run test:graphics: 홈 → 설정 → 그래픽의 네 모드, 실제 렌더링 예산·전환·기본값 복원·저장/새로고침, 휴대폰 회전 안내·가로 배치와 마크X발로 전환 중 지형·플레이어 상태 보존 검사. 휴대폰은 브라우저 에뮬레이션이며 실기기 FPS 검사가 아닙니다.npm run test:home-layout: 상단 메뉴와 원래 플레이 버튼·로고·요원·계정 칩 스타일 유지, 5개 화면 크기의 경계·겹침·실제 메뉴 클릭 검사. 수정 화면.npm run test:startup: 엔진 로딩 전 무기고 버튼 표시, 스킨 바로가기, 지연된 보유품 조회·뽑기 및 화면에 보이는 최종 요원 사진 검사.npm run test:followup: 방 목록 갱신·정렬·실패 표시, 요원 전용 그래픽 자원 해제, Chromium CDP 멀티터치·취소, 배치 편집·복원, 계정 전환·다른 탭 로그아웃과 한국어/영어 화면 검사. API는 테스트 저장소로 대체하며 서비스 워커를 막아 요청을 정확히 대체합니다. 모바일 실기기나 운영 데이터 검증을 뜻하지 않습니다.js/game-rules.js: 입력·능력·승패·피니셔의 공통 규칙.js/game-events.js: 온라인 처치 정보, 피니셔, 재접속 보호, 결과 완료 사건.js/home.js, js/home-details.js, css/home.css: 홈 동선, 최근 설정·즐겨찾기·모드 안내·장착 스킨과 미리보기.js/home-navigation.js, css/home-navigation.css: 상단 메뉴·계정 배치와 원래 홈의 위치·크기 조정.js/damage-cause.js: 지연 피해의 발사 당시 무기·스킨.js/modal-focus.js: 주요 모달의 키보드 포커스.js/portrait-loader.js: 부팅 후 화면에 보이는 요원 사진을 작업별로 생성하고, 교체된 카드의 대기 작업 정리.js/game-boot.js: 모든 스크립트 로딩 뒤 프레임·초대 연결 시작.npm run test:regression: 추가 홈 기능·실제 RPG 폭발·부활·수류탄·반복 경기 검사.npm run test:network-recovery: 실제 게임을 두 브라우저에서 실행하고 통제된 전송으로 채널 단절·재접속·진행 중 참가를 검사합니다. WebRTC/ICE/TURN 성공 검사는 아닙니다. 멀티 검사 기록.전체 작업 계획과 남은 항목은 개발 메모 #17에 기록합니다.
실제 Redis의 Lua 저장 경로는 임시 Redis 인스턴스를 실행한 뒤 DFDF_REDIS_CLI와 DFDF_REDIS_SOCKET을 지정해 npm test로 검사할 수 있습니다. 지정하지 않으면 해당 통합 테스트 하나를 건너뜁니다. 테스트 API 요청은 운영 서버로 전송하지 않습니다. test:online은 브라우저가 WebRTC 연결을 허용하는 환경이 필요합니다. 관리 정책으로 직접 UDP가 차단되고 TURN도 없는 환경에서는 연결 실패를 보고하며, 성공한 검증으로 간주하지 않습니다.
소유자와 관리자 인증 형식을 갱신했으므로 적용 후 해당 계정은 다시 로그인해야 합니다. 일반 계정은 비밀번호 초기화 또는 새 세션 발급에 따라 이전 세션이 무효가 될 수 있습니다. 보상은 유효한 경기 확인표가 있어야 지급되며, P2P 호스트의 경기 자체를 서버가 검증하는 구조는 아닙니다.
RR은 서버에서 발급한 랭크 경기 확인표와 최소 실제 참가자 조건을 만족한 경기에만 반영됩니다. AI전·일반 비공개 방은 랭크 경기로 계산하지 않으며 사격 연습과 맵 탐방은 경기 보상을 지급하지 않습니다.
세션 버전이 없는 이전 로그인은 다시 로그인해야 합니다. 경기 보상 확인표도 해당 계정의 세션 버전에 묶으므로, 비밀번호 초기화 또는 삭제·재가입 이전의 확인표는 사용할 수 없습니다. 업데이트 전에 발급한 이전 형식의 확인표는 새 경기 시작 후 새 확인표로 대체됩니다.
전설보다 높은 초월 등급의 팬텀 공격·수비 스킨을 지원합니다. 경기 완료마다 초월 코인 1개를 받고 스킨 한 종류를 700개로 구매하거나, 뽑기 0.00000000000000001% 확률로 획득합니다. 총 스킨 창 위쪽 챔피언스 탭에서 제공된 원본 모델·텍스처를 자동으로 불러와 구매·미리보기를 하고, 뽑기는 기존 뽑기 탭에서 함께 진행합니다. 별도 등급표와 전용 킬 배너·원본 용/뱀 마무리·원본 1~5킬 연주, 로컬 JAR 교체도 지원합니다. 마무리는 플레이어 키의 최소 5배 크기이며 일반전 마지막 처치 후 8초 뒤 다음 라운드(마지막 라운드는 결과 화면)로 전환합니다. 사용 방법과 원본 자산 처리, 테스트는 연결 안내를 참고하세요.
타격대 네온은 이 게임이 직접 만든 요원입니다. 외형·초상화·1인칭 팔과 스킬 동작·아이콘·전기 효과·미끄러지기 자세를 모두 이 저장소의 코드로 그리고, 바깥에서 가져온 모델·텍스처·애니메이션·효과 코드는 쓰지 않습니다(능력의 동작과 수치만 참고했습니다). Q 되튐 전격(한 번 튕기는 전기탄, 닿는 곳마다 0.8초 뒤 뇌진탕), E 질주 회로(10초 +40%, 좌클릭으로 6m 미끄러지기), C 전류 통로(25m 전류 벽 두 개, 4초 시야 차단), X 과충전(20초 40m 빔). 자세한 내용은 docs/NEON_AGENT.md, 검사는 npm run test:neon, test:neon-look, test:neon-online.
/play.html?ascent-trial=1 → 이 맵으로 연습 시작에서 제공된 schematic 지형을 우리 이동·사격 엔진으로 시험합니다. Minecraft 1.20.1 기본 블록 텍스처 239개를 사용하며 일반 진입은 추가 맵 데이터를 내려받지 않습니다. 변환 방식과 솔로 검증 범위는 시험 맵 안내를 참고하세요.
제공된 Valorant 팩의 총 9종과 나이프 스킨 6종을 연결했습니다. 정찰 소총과 팬텀은 표시 이름을 맞바꾸었고, 챔피언스는 팬텀(scout)에 장착합니다. 총기 18종과 별도 나이프를 제공하며 기관권총·위스퍼 기관단총·점사소총·산탄총·정찰 소총을 복원했습니다. 기존 보유 스킨은 보존하고 통합 당시 보관한 원본 보유 목록에서도 한 번 복원합니다. 구매창은 카테고리별 총 실루엣 카드와 선택한 장착 스킨의 실제 모델 미리보기를 제공합니다. 원본 파일 보존과 적용 범위는 무기 팩 안내를 참고하세요.
2026-10-21의 구현 내용·24개 개선 항목·실행한 검사와 남은 환경은 개선 적용 기록에 정리했습니다.
홈의 성능 진단에서 최근 1,800 프레임의 중앙값·p95, 긴 프레임 수, 자원 수와 연결 단계를 기기에만 기록하고 JSON으로 저장할 수 있습니다. 자동 조절은 탄피와 그림자 갱신 빈도를 먼저 줄이고, 부담이 계속되면 후처리를 끕니다. 휴대폰 모드의 선명도와 능력 판정은 유지합니다. 연습 → 빠른 훈련에 표적장과 능력·스파이크 연습이 있고, 마크X발로는 홈에서 직접 선택할 수 있습니다.
일반~전설 수집을 끝낸 계정도 기존 확률로 초월 뽑기를 시도하며, 받을 일반 스킨이 없는 미당첨 회차는 골드를 반환합니다. 700코인 구매와 경기 완료당 1코인은 그대로입니다.
재현: npm ci 후 npx playwright install chromium, npm run check, npm run check:assets, npm test, npm run test:suite(핵심) 또는 npm run test:full(전체). Node 20 이상. API 계정·구매는 격리된 fixture이며 실제 결제를 하지 않습니다. GitHub CI는 PR 핵심 검사, 야간 전체 검사와 임시 Redis Lua 통합 검사를 실행합니다.
게임이 여는 모든 WebRTC 연결(방장과 게스트)은 js/legacy/110-…js가 기록합니다. 만들어짐, 찾은 후보 종류별 개수(host / srflx / relay), ICE 상태 변화, 연결되거나 끊길 때 실제 쓰인 경로(로컬·원격 후보 종류, 왕복 시간), 앱이 직접 닫았는지(closed-by-app)를 남깁니다. 기록은 브라우저 안에만 있고 어디로도 보내지 않으며 최근 300줄입니다. 콘솔에서 [ice]로 보거나 iceLogText()로 전체를 복사할 수 있습니다. 테스트는 npm run test:ice-log입니다.
연결 기록 신고: 연결이 끊기면(ICE failed/disconnected, 또는 방장을 잃음) 마지막 40줄을 요약해 sessionStorage(lgp_ice_report)에 둡니다. 새로고침해도 남고 30분 뒤 무시됩니다. 요약에는 시각·연결 번호·역할·이벤트·후보 종류(host/srflx/relay)·프로토콜·왕복 시간·후보 개수만 있고, 후보 주소와 서버 주소는 없습니다(서버 목록은 개수만). 방장을 잃은 게스트에게는 연결이 끊겼습니다 · 연결 문제 신고 카드(js/legacy/111-…js, 방장이 일부러 나간 경우는 제외, 새로고침 뒤에도 한 번 다시 보임)가 뜨고, 누르면 버그 신고 양식이 어디서: 연결 끊김으로 열립니다. 양식(js/feedback.js)은 저장된 기록이 있으면 연결 기록 첨부됨 (N줄)을 보여 주고, 첨부하지 않기로 뺄 수 있습니다. 서버(cleanIce)는 버그 신고의 ctx.ice만 받아 정해진 모양(이벤트 이름·후보 종류·프로토콜·숫자)에 맞는 최대 40줄만 저장하고 나머지는 버립니다. 운영자 페이지 문의 카드는 이를 표(시각·pc·역할·이벤트·경로)로 보여 주고, 연결 기록 있는 것만 체크로 거릅니다. 테스트는 npm test(tests/feedback-ice.test.js)와 npm run test:ice-report(끊김은 실제 RTCPeerConnection에 failed 상태를 알려 흉내 냄)입니다.
gone 표시, 서버는 RETIRED).js/legacy/67-knives.js, 83-knife-tricks…js, 58-gun-skins.js).에임 보정 스위치로 켜고 끕니다(기본 켬, 세기 약하게·보통·강하게). 컴퓨터는 운영자가 '가입자 → 에임 보정'에서 허용한 계정(기한 지정 가능)이나 관리자 모드에서만 쓸 수 있고, 관리자 조절판의 개인 설정에도 스위치가 있습니다. 랭크전에서는 컴퓨터 보정이 항상 꺼집니다. 운영자는 '게임 설정 → 에임 보정'이나 대시보드에서 모바일·컴퓨터를 따로 끄고, 세기(%)·거리(m)·각도(도)를 정하고, 계정마다 모바일 보정을 막을 수 있습니다 (js/legacy/84-aim-assist…js, api/v1.js의 aim).a_summary 한 번(계정은 MGET 한 번)으로 가져오고, 사용량을 아끼려고 자동으로 새로 고치지 않습니다.프로필 창이 열립니다. 탭은 정보(계정·레벨·골드·제한과 권한·랭크와 운영자만 보는 숨은 MMR·스킨 수와 장착·찜·미션·꾸미기·문의), 접속 기록(로그인·가입·실패, 최근 IP·기기 10개), 뽑기·상점(뽑기 횟수·등급 분포, 번·쓴 골드, 구매), 경기·랭크(최근 경기, RR·MMR 변화), 주문, 전체 로그(종류 거르기·검색·CSV 내보내기)입니다. 서버는 계정마다 ulog:<아이디> 목록(최근 500줄, 마지막 줄에서 180일 보관)에 로그인·가입·게스트·로그인 실패(IP·기기), 뽑기(결과 목록·골드), 상점·꾸미기 구매, 스킨 장착, 찜, 미션 보상, 경기 결과(랭크전은 RR·MMR 전후), 레벨·티어, 방, 주문, 문의, 운영자가 계정에 한 작업(차단·권한·골드·RR·스킨 지급 등)을 한 줄씩 남깁니다(토큰·비밀번호는 넣지 않음). 창은 관리자 호출 a_user_profile 한 번으로 계정·기록·합계를 받고, 개별 기록이 시작되기 전 기간은 서버 로그와 계정의 최근 경기·가입 시각에서 가져온 것만 보이며 기록 시작 이전으로 표시합니다 (api/v1.js의 ulog·ADMIN.user_profile, admin.html의 uname·openProfile, tests/api.test.js, npm run test:user-profile).js/legacy/85-knife-fight-and-gun-game…js, 05-game-modes.js).wish, 58-gun-skins.js).js/legacy/86-runaway-train…js).93-two-more-maps…js). 야간 항구(3대3)는 달빛 아래의 부두입니다. 서쪽은 바다(난간과 정박한 배, 먼바다의 큰 배), 방어 쪽에 불 켜진 창고 둘, 가운데 겐트리 크레인(위의 탐조등이 마당을 훑고, 꼭대기 표지등이 깜빡임), 공격 쪽에 컨테이너 줄. 가로등 11개가 따뜻한 빛을 떨구고 A 사이트 옆 하나는 깜빡입니다. 옥상 도시(5대5)는 4.5~9m 건물 16채가 있는 오후의 도시입니다. 건물마다 벽을 따라 오르는 계단이 있어 옥상에 올라갈 수 있고(옥상에는 난간과 환기구 엄폐물), 사이트 A·B는 건물 사이의 광장, 가운데 큰길과 가로지르는 거리, 가로지르는 거리 위로 집라인 2개(F)가 있습니다. 봇은 길을 다니고 옥상은 사람의 자리입니다. 두 맵 모두 모든 노드·사이트·스폰이 걸어서 닿고 레인은 5m 이상인지, 어떤 벽도 우연히 겹치지 않는지, 계단이 옥상까지 0.68m 이하로 이어지는지 test:newmaps가 확인합니다.npm run audit:ui, audit:ui-home): 폰(568x320 ~ 932x430, 터치)·태블릿·PC 크기에서 주요 화면(타이틀, 요원 선택과 맵·모드 시트, 설정, 무기고 각 탭, 같이 하기, 구매, 인게임, 결과, 홈페이지)을 열어 잘린 버튼, 다른 것에 가려져 눌러도 닫히거나 안 눌리는 버튼, 너무 작은 터치 대상, 서로 겹친 버튼, 가로 넘침을 목록으로 뽑습니다 (ui-audit-lib.js). 이걸로 찾아 고친 것: 작은 폰에서 맵·모드 시트가 요원 카드·즐겨찾기 줄 아래로 깔리던 것, 시트 칩이 두 줄로 부서지던 것, 568x320에서 역할 탭·스킨 버튼 줄이 세 줄이던 것, 결과 화면의 다시 하기가 표 아래 화면 밖이던 것, 홈페이지 가로 모드에서 지갑 칩이 잘리던 것, 플레이 안내 카드가 위쪽 바 밑으로 넘치던 것, 터치에서 25px 이하이던 탭·칩(36px 이상으로). test:ui-fixes가 이 수정들을 지킵니다.94-capture-the-flag…js, 95-shrinking-zone…js, 안전망 75는 축소전에서 부활시키지 않음).문의 · 제안, 게임 첫 화면 메뉴의 문의, 설정 창의 문의에서 같은 양식(js/feedback.js)이 열립니다. 종류는 버그 신고(어디서·재현 방법), 요원 제안(이름·역할·Q/E/C/X 능력 이름과 설명), 사용자 신고(대상 이름·이유·방 코드), 추가했으면 하는 점(제안, 저장 이름은 idea), 후기(review, 내용 필수·별점 1~5 선택, 별점은 rating으로 저장), 문의, 기타. 로그인했거나 게스트로 들어온 사람만 보낼 수 있고(한 시간에 5건·하루 15건, 같은 내용 10분 안에 두 번은 막음), 보낸 사람의 이름·레벨, 화면 크기, 하던 모드·맵이 함께 저장됩니다. 보낸 사람은 내 문의 탭에서 처리 상태와 운영자의 답장을 봅니다. 운영자 페이지의 문의함(새 문의 개수 배지)에서 종류·상태로 거르고 검색하고, 열면 확인함이 되며, 처리 중·완료·보류를 고르고 메모(비공개)와 답장을 남기거나 지웁니다. 서버는 feedback_submit·feedback_mine과 관리자용 a_feedback·a_feedback_set·a_feedback_del, 문의는 90일 보관, 최근 3000건 (test:feedback).js/notice-popup.js)이 한 번 뜹니다. 내용은 운영자의 공지 문구(game.announcement, 예전의 위쪽 띠는 없어짐)와 홍보영상 카드(game.promoNotice가 켜져 있고 js/promo.js의 PROMO.video가 http로 시작할 때)입니다. 영상은 항상 16:9 가로 영상이고(폰을 세워도 같음), 포스터 위 재생 버튼을 눌러야 재생되며(preload="none", 자동재생 없음, 줄임 모션 설정이면 videoCalm, 자막 captions), 아래 후기 남기기·추가했으면 하는 점 보내기 버튼은 문의 양식을 후기·제안 종류로 엽니다. X·Esc·바깥 클릭으로 닫으면 내용(공지 문구 + 영상 주소)의 해시를 localStorage(lgp_notice_seen)에 기억해 같은 공지는 다시 안 뜨고, 공지가 바뀌거나 새 영상이면 다시 뜹니다. 로그인 화면·차단·점검 중에는 뜨지 않습니다. 홈페이지 위쪽 ▶ 홍보영상 버튼(좁은 폰에서는 ▶만)은 영상 부분만 다시 열고 아무것도 기억하지 않습니다. 운영자 페이지 대시보드 → 홍보영상 공지 스위치(또는 게임 설정 → game.promoNotice)를 끄면 영상 카드와 버튼이 사라지고 공지 문구만 남습니다. 게임 화면(play.html)의 위쪽 공지 띠(acct-notice)는 그대로이고 X로 이번 방문 동안 치울 수 있습니다. 영상 주소는 js/promo.js 맨 위의 window.PROMO에서 바꿉니다 (test:promo).게임 설정 → order에서 판매 열기를 켜고 입금 계좌·예금주·안내 문구와 치트별 가격(7일 기준, 원)·기간 배수(1일·7일·30일·영구)를 정합니다(계좌는 저장소가 아니라 이 설정에만 둡니다). 열리면 홈페이지 위쪽과 게임 메뉴에 치트 구매가 나타나고(꺼져 있으면 보이지 않음), 계정으로 로그인한 사람이 치트 9가지(레이더·벽 투시·치트 모드·한 방·탄약 무한·반동 없음·이동 속도 ×1.5·연사 ×2·능력 재충전 10초)와 기간, 또는 골드 팩(1,000·5,000·20,000)을 골라 금액이 계산되면 주문합니다. 무적은 판매하지 않으며 골드는 기간 없이 한 번만 결제하고 열어 주는 순간 지갑에 더해집니다(환불·회수하면 그만큼 빠집니다). 관리자 모드도 판매하지 않습니다. 운영자가 운영자 페이지에서 계정에 직접 주는 경우에만 열리고, 줄 때 그 계정에 모든 치트(무적 포함)가 같은 기간 동안 함께 열리며 관리 기능은 운영자가 고른 항목(기본은 전부)이 열립니다. 관리자를 거두면 그 전에 갖고 있던 치트로 돌아갑니다. 주문 번호·입금 계좌·금액·입금자명이 화면에 나오고(복사 버튼), 내 주문에서 상태(입금 대기·열림·거절·취소·환불)를 보며 대기 중인 주문은 취소할 수 있습니다(대기 3건까지, 하루 10건까지, 게스트는 불가). 운영자 페이지 구매 주문(대기 개수 배지)에서 주문을 보고 입금자명·금액을 통장과 맞춘 뒤 입금 확인 · 열어주기를 누르면 그 계정의 치트 권한(perm, 운영자가 손으로 주던 바로 그것)에 합쳐지고 기간이 더해집니다. 거절(사유가 보임)·환불(치트도 함께 끌지 선택)·메모도 있습니다. 랭크전에서는 치트가 꺼지는 기존 규칙이 그대로입니다 (order_info·order_create·order_mine·order_cancel, 관리자 a_orders·a_order_set, js/order.js, test:orders).장부에 주문 접수·입금 확인·회수·환불·거절·취소가 한 줄씩 영구히 남습니다(시각·종류·주문번호·계정·내용·기간·입금자·금액·사유). 위쪽에 입금 확인 합계·환불 합계·순매출·접수·회수·거절 건수가 나오고, 종류·기간(오늘·이번 달·지난 달)·검색으로 거르며 CSV 내려받기로 엑셀용 파일을 받습니다. 장부가 생기기 전의 주문은 처음 열 때 한 번 채워 넣습니다. 구매 주문의 열어 준 주문에는 회수(그 주문으로 열린 치트와 골드만 거두고 입금 기록과 돈은 그대로)와 환불이 따로 보이고, 회수된 주문도 환불할 수 있습니다. 환불에서 "치트도 함께 끔"을 고르면 예전처럼 그 계정의 치트를 모두 끕니다 (a_ledger, a_order_set 의 revoke, test:orders).js/legacy/87-two-more-agents…js).ensureWallet의 seasonRoll), 첫 화면과 홈페이지에 한 번 알림이 뜹니다. 티어 칩과 홈페이지에 시즌 번호·남은 날·이번 시즌 랭크전 수가 보입니다. 운영자는 대시보드의 시즌 칸에서 티어별 골드, 전용 스킨 받는 티어를 바꾸고 지금 시즌 끝내기로 오늘부터 새 시즌을 시작하며, '게임 설정 → 시즌'에서 길이·남는 RR 비율·최소 판수를 정합니다. 스킨은 현재 607개입니다 (api/v1.js의 season, js/legacy/88-seasons…js).58-gun-skins.js의 KITS.champion; 총 위에 얹는 껍데기라 판정·반동은 그대로입니다). 쏠 때 총구에서 금색·보라 빛의 고리 세 개가 시간차로 커지며 사라지고, 이 스킨으로 처치하면 처치 자리에서 바닥으로 물결 고리가 퍼지며, 킬배너도 검정·금·보라 전용입니다. 다른 사람이 쏘는 것도 같이 보입니다 (스킨 번호로 판별, 서버 검증 vouch). 소리와 모양만 바뀌고 전투에는 영향이 없습니다. 외부 게임·모드의 모델·텍스처를 쓰지 않고 처음부터 그린 디자인입니다. 뽑기·상점에 나오고 스킨 아이템은 현재 607개입니다 (58-gun-skins.js의 champion, js/legacy/96-champions-wave-…js, npm run test:champion).js/legacy/97-legendary-finishers…js, npm run test:finisher).api/v1.js의 weeklyOf).89-infection…js). 점령전은 땅에 그려진 점령 지점을 혼자(또는 더 많이) 차지하면 우리 색이 되고, 차지한 쪽이 초당 1점, 120점 먼저. 지점은 90초마다 6초 예고 뒤 옮겨 가고 처치는 점수가 아닙니다 (90-conquest…js). 두 모드 모두 방장이 정하고 손님에게 알립니다.91-target-range…js).꾸미기 탭에서 칭호(41)·네임카드(24)·킬 효과(8)를 골드로 삽니다(일반 600 · 희귀 1,800 · 영웅 4,500 · 전설 10,000). 칭호는 이름 아래, 네임카드는 홈 프로필과 결과 화면 배경, 킬 효과는 처치 배너 색으로 나옵니다 (본인 화면에만). 소유·착용은 서버가 계정에 저장하고 목록은 js/cosmetics.js와 서버(COS)가 같은지 테스트로 확인합니다 (cos, cos_buy, cos_equip, 92-profile-cosmetics…js).js/legacy/99-two-storey-buildings-…js, npm run test:buildings). 같이 한 성능 개선: 벽 판정에서 메모리 할당 제거와 범위 밖 빠른 제외, 몸 밀어내기를 주변 칸만 검사, 전술 시점 벽을 한 장으로 그리기, 수류탄 칩 글자를 바뀔 때만 쓰기 (npm run test:raycast).요원 탭에 요원 28명이 역할별(타격대·척후대·전략가·감시자)로 나오고, 요원마다 체크로 켜고 끕니다. 끈 요원은 요원 선택 화면, 봇 배정, 무작위 선택, 경기 중 요원 바꾸기에서 빠지고, 다시 켜면 원래 자리로 돌아옵니다(설정 game.disabledAgents, 요원 아이디를 쉼표로). 최소 10명은 켜 두어야 하고 역할마다 1명 이상 남아야 저장되며(서버가 거절), 설정이 잘못돼 요원이 모자라면 게임은 전원을 그대로 둡니다. 게임은 접속할 때와 신호를 받을 때 이 설정을 읽고, 경기 도중이 아니라 타이틀·로비 화면에서 반영합니다(이미 진행 중인 경기는 그대로). 새 요원을 만들면 js/agent-roster.json(운영자 페이지가 읽는 명단)도 같이 고쳐야 하고, 어긋나면 npm run test:agentswitch가 알려 줍니다 (js/legacy/108-agents-…js).js/legacy/106-browser-shortcuts-…js, npm run test:crouch).js/legacy/105-the-operator-kills-with-any-hit.js, npm run test:operator).js/legacy/100-golden-chests-…js, 황금 권총은 상점·스킨 목록에 나오지 않습니다, npm run test:chest).scripts/gen-hospital.js가 만들어 js/hospital-plan.js에 굽고(2.5m 칸, 벽은 칸 가장자리라 1칸 복도와 문도 봇이 지나감), 그리는 쪽은 js/legacy/94-abandoned-hospital-…js입니다. npm run test:hospital이 모든 문턱(1만 3천 곳)을 실제 이동 코드로 걸어 보고, 계단 4곳, 봇의 사이트 도달, 입구 차단, 정리를 확인합니다.js/legacy/100-first-person-hands-and-gun-finish.js).js/legacy/96-character-details-…js).게임 들어가기가 y=728(창 밖)에 있었습니다. 폭 1000px 이상·높이 600px 이상 창에서는 왼쪽에 큰 제목, 오른쪽에 프로필 카드와 입구 버튼을 두어 1280×720에서 y=481npm run test:landing이 여덟 가지 창 크기에서 버튼이 보이는지와 가로 스크롤이 없는지를 확인합니다.GFX.lamps)을 따라 세 단계로 갈립니다. ① 성능·최소: 이전과 같음(광원 3개 이하) ② 균형(기본값): 512px + 타일 모서리·균열·알갱이 디테일, 이방성 필터 8배, 광원 4개 ③ 비주얼·최고: 1024px + 법선 맵·거칠기 맵(바닥의 젖은 자국은 거울처럼 반사), 이방성 16배, 광원 7~9개(응급실·하역장·중환자실·병동 둘 추가), 둥근 소품 분할 수 복원. 프리셋을 바꾸면 맵이 다시 지어지고 약한 단계로 돌아가면 큰 질감은 해제됩니다. 병목은 계산이 아니라 GPU 캔버스 읽기였습니다(getImageData가 13.8초 → willReadFrequently로 0.4초). 벽의 날카로운 '벗겨진 페인트' 다각형은 부드러운 습기 자국으로 바꾸고 벽 칸마다 질감을 8분의 1 단위로 어긋나게 해서 2.5m 반복을 줄였습니다.스킨 버튼이나 홈의 무기고가 여는 총 스킨 창 위쪽에 기본 · 기본 음색 · 피니셔 없음이라는 글자 한 줄과 작은 버튼 넷이 있었습니다. 지금은 스킨 등급 색으로 물든 카드입니다: 스킨 이름과 등급 칩, 총 이름과 그 총소리의 특징 설명(gunshot-synth.js의 설명), 음색·피니셔 태그, 그리고 아이콘이 있는 분절 버튼(총소리·처치음·피니셔·정지). 재생 중인 버튼은 막대가 움직이고, 음색 팩이 없는 스킨은 처치음·피니셔가 꺼지며 이유를 툴팁으로 알려 줍니다. 낮은 화면(가로 휴대폰)에서는 한 줄로 줄어듭니다(js/home.js, css/home.css). 총소리 모듈을 63번과 80번 사이로 옮겨서(63b-…js), 스킨 음색 팩이 새 총소리 위에 그대로 얹히고 미리듣기도 새 총소리를 냅니다. test:gunshots가 이를 확인합니다.104-death-shows-who-killed-you-…js): 쓰러진 자리에서 처치한 상대 쪽으로 화면이 미끄러지듯 이동해 그 캐릭터를 몇 미터 앞에서 천천히 돌며 보여 주고, 머리 위에 닉네임과 그 옆에 칭호(등급 색), 아래에 요원·무기 이름이 뜹니다. 2초가 지나면 원래대로 아군 시점·부활로 이어지고, 그 동안은 아군 인계도 미룹니다. 월드·낙사처럼 처치한 상대가 없으면 나오지 않습니다. 접속한 사람의 칭호는 입장 메시지와 참가자 목록에 실려 모두에게 보이고(카탈로그에 없는 칭호는 버립니다), 봇은 계정이 없어 아이디로 카탈로그에서 칭호 하나를 받습니다(모든 화면에서 같음). test:killcam이 확인합니다.109-the-big-5v5-maps-drawn-smaller-…js): 빙하·네온·모래무덤·연화원·한옥 시장·밀림의 바닥 거리가 0.75배입니다(높이는 그대로). 벽·노드·사이트·스폰·경계, 발판(51), 문·텔레포터(06), 집·감시탑 위치(99), 테마 구조물과 소품이 같은 비율로 옮겨져 레이아웃 모양은 같고, 스폰에서 사이트까지 걷는 길이 약 1/4 짧아집니다. 집과 소품은 크기를 유지하고 자리만 옮깁니다. 봇 길찾기 격자 칸도 같은 비율(2.5 → 1.875m)로 줄여 좁아진 통로를 봇이 그대로 지나갑니다. 들판·포구·옥상 도시·폐병원·마크X발로와 3대3·1대1 맵은 그대로입니다. 게임을 불러오기 전에 window.MAP_SHRINK로 비율을 바꿀 수 있고 1이면 꺼집니다. test:map-shrink가 줄이기 전과 후를 같은 방식으로 지어, 경계·스폰-사이트 거리가 0.75배인지, 모든 스폰 자리에서 모든 사이트로 여전히 걸어가고 길이 짧아졌는지, 반경 0.75m 몸으로 걸을 수 있는 바닥이 잘려 나간 곳 없이 이어지는지, 벽 속 스폰·경계 밖 소품이 없는지, 집·문·텔레포터 수가 같은지 확인합니다.js/select-mapshot.js, css/select-mapshot.css): 고른 맵을 공격 쪽 뒤에서 약 50° 내려다본 장면을 게임 렌더러로 한 장 찍어 요원 선택 화면의 배경으로 깝니다. 맵 칩 줄의 왼쪽 위에 맵 이름·팀 인원·크기(예: 빙하 관측소 5대5 · 288×171m)가 뜹니다. 다른 맵을 고르면 고르기를 멈춘 뒤(0.35초) 그 맵을 불러와 새로 찍고, 한 번 찍은 맵은 다시 불러오지 않고 바로 보여 줍니다. 맵을 불러오는 것은 메뉴에서만이고 경기 중에는 하지 않습니다. 휴대폰에서는 맵 고르기 시트 맨 위에 그 장면이 카드로 나옵니다. test:select-mapshot이 확인합니다.js/ability-visibility.js): 둔화·속박·뇌진탕·무기 봉인·취약·보호막·무적·노출·실명은 맞은 캐릭터의 숫자만 바꾸고 몸에는 아무것도 그리지 않아, 맞혀도 빗나간 것처럼 보였습니다. 이제 걸린 캐릭터에 표시가 붙습니다: 둔화는 발밑 하늘색 고리와 도는 얼음 조각, 속박은 초록 고리와 가시, 뇌진탕은 머리 위를 도는 별, 무기 봉인은 노란 고리, 취약은 몸통의 파란 고리, 보호막은 몸을 감싼 투명한 구(무적은 금색), 노출은 머리 위 빨간 마름모, 실명은 눈높이의 흰 섬광입니다. 머리 위에는 걸린 상태 이름이 한 줄로 뜹니다(둔화 · 노출). 다른 사람이 능력을 쓰면 그 능력 이름이 요원 색으로 머리 위에 떠올랐다 사라집니다(궁극기는 크고 금색, 내 시전에는 뜨지 않음). 조준선 아래 내 상태 줄에 둔화·속박·실명·보호막·노출이 추가됐습니다. 1인칭의 내 몸에는 그리지 않고 벽 너머로는 보이지 않습니다. test:ability-visibility가 확인합니다.103-spider-web-swing-…js). 두 손에서 거미줄이 동시에 나가 0.16초 동안 날아가 벽에 붙고(손목 섬광, 붙는 자리의 거미줄 자국), 날아갈 때는 늘어져 출렁이다가 당겨지면 팽팽해지며 가늘게 떨립니다. 몸은 줄에 매달려 스윙합니다: 중력과 줄의 길이 제한과 줄 감기로 움직여서 처음에는 무게가 실려 천천히 가다가 곡선을 그리며 빨라집니다(약 5 → 25m/s). 점프로 놓으면 속도를 유지한 채 던져지고, 벽에 닿으면 전처럼 벽에 매달립니다. 스윙하는 사람의 화면은 시야각이 약 6° 넓어지고 살짝 기울며 흔들리고 가장자리에 속도선이 들어옵니다(동작 설정을 끄면 모두 꺼짐). 소리(발사·붙는 소리·줄이 당겨지는 소리)는 합성이며, 쿨다운·사거리 48m·충전 수는 그대로입니다. test:spider-swing이 확인합니다.x_silk_pull을 다시 쓴 것입니다 (js/legacy/109-spider-silk-…js, npm run test:spider-silk).기본 · 돌격소총으로 보이던 모순 제거), 패널 자체가 초월 색(#ff7bd5)으로 물든 카드가 됩니다. 이름·초월 칩·카빈 전용 표시, 설명과 태그(공격/수비·처치 배너·용 마무리·전용 사운드), 공격·수비 전환 스위치, 소리(발사음·킬 배너·소리·마무리)와 연출(살펴보기·재장전·중지) 아이콘 버튼 줄, 재생 중 막대 표시가 들어 있습니다. 구매 카드도 같은 톤입니다(js/champions-phantom.js, css/champions-phantom.css). 페이지 전체도 같은 재질로 맞췄습니다: 제목 머리말(초월 등급 · 카빈 전용), 총 미리보기 뒤의 초월색 후광, 등급표 막대, 진영 색점이 있는 구매 버튼. test:champions-panel이 확인합니다.js/gunshot-synth.js가 총성의 구조를 모델링해서 총마다 따로 만듭니다: ① 충격파의 날카로운 '탁'(초음속탄만) ② 총구 폭발 ③ 피치가 떨어지는 저음 ④ 노트북 스피커용 중저음 '쿵' ⑤ 슬라이드·노리쇠·펌프 소리 ⑥ 방의 울림(병원은 1.5초짜리 실내, 다른 맵은 0.55초 야외). 총당 두 테이크를 번갈아 쓰고 재생 속도와 크기를 조금씩 흔들어 연사가 같은 샘플의 반복이 되지 않게 했습니다. 거리가 멀면 늦게 도착하고 둔하고 작고 울림이 많아지며, 난전에서는 먼 소리부터 버립니다(동시 40음성). 녹음이 아니라 합성입니다: 라이선스를 확인할 수 없고 이 환경에서 소리를 들어 볼 수 없어서, 실제 무기(구경, 총열 길이, 소음기, 볼트·펌프)에서 숫자를 정하고 tests/gunshot-synth.test.js로 측정했습니다(31종이 어느 두 쌍도 길이·밝기·저음 중 하나는 다름, 소음기는 고음이 반 이하, 구경이 클수록 저음과 여운이 큼). gunshot-lab.html에서 총마다 거리·방을 바꿔 가며 들어 볼 수 있습니다. npm run test:gunshots는 게임 안 연결을 확인합니다.ELMWOOD GENERAL HOSPITAL)을 달았습니다. 얇은 상자와 평면 하나(수백 삼각형)이고 충돌체는 없습니다(전부 머리 위 높이이거나 벽에 붙어 있음). 간판 글자는 캔버스 폭에 맞춰 줄입니다.HOSPITAL_NAV.opts.posts로 끌 수 있습니다. 작은 지도와 둥근 전술 지도는 모든 층의 벽·바닥이 겹쳐 덩어리로 보였는데, 지금 서 있는 층만 그리고 작은 지도 아래 띠에 층과 방 이름을 적습니다(js/legacy/97-bots-climb-…js). npm run test:hospital-bots가 3개 층 그래프, 거점 도달·정지·시선, 지도를 확인합니다.이 맵으로 시작 버튼이 있고, 플레이 안내는 최신 3개만 보이다가 지난 소식 보기로 펼쳐집니다. 요원 그림은 이 열 밑으로 들어가지 않고 비켜 섭니다 (js/home-newmap.js, css/home-newmap.css).맵 · 모드 ▾ 버튼 하나로 접히고, 누르면 아래에서 올라옵니다 (css/review-fixes.css, js/select-compact.js).css/review-fixes.css, css/armory-mobile.css, js/scroll-fade.js, 검사 npm run test:design-polish):
--fs-xs(11px)부터 --fs-2xl(24px)까지를 두었습니다. 모서리는 기존 --r-*를 씁니다.js/portrait-loader.js).상점·뽑기로 가는 안내 카드가 붙습니다.npm run test:ranked-flownpm run test:seasonnpm run test:online: 두 브라우저의 실제 WebRTC 연결 검사. 테스트용 peer 패키지와 PeerJS 브라우저 파일이 필요하며 DFDF_PEER_SERVER_MODULE, DFDF_PEERJS_PATH로 경로를 지정할 수 있습니다. 로컬 신호 서버를 9000번 포트에서 실행합니다.js/game-audio.js, js/client-request.js: 공통 오디오 출력과 요청 시간 제한.