컬러닷지: 두 판 사이의 차이

리버티게임, 모두가 만들어가는 자유로운 게임
잔글 (BANIP님이 사용자:BANIP/컬러닷지 문서를 넘겨주기를 만들지 않고 컬러닷지 문서로 이동했습니다: 배포)
잔글 ("컬러닷지" 문서의 보호 설정을 변경했습니다: AuthorProtect 전체 적용 설정의 문제로 인해 자동 인증된 사용자 이상의 권한으로 보호 설정 변경 ([편집=자동 인증된 사용자만 허용] (무기한) [이동=자동 인증된 사용자만 허용] (무기한)))
 
(사용자 2명의 중간 판 4개는 보이지 않습니다)
1번째 줄: 1번째 줄:
{{게임 정보}}
{{#css:
{{#css:
.gamebody{
.gamebody{
111번째 줄: 112번째 줄:
{{플러그인X|/스크립트}}
{{플러그인X|/스크립트}}
ios는 지원되지 않습니다.
ios는 지원되지 않습니다.
{{장르 분류}}
[[분류:리버티게임]]

2024년 10월 27일 (일) 13:24 기준 최신판

Applications-system.png
원개발자 이외에는 편집을 할 수 없는 게임
이 게임은 원개발자 이외에는 편집을 할 수 없는 게임입니다.
이 게임을 잘못 수정하면 게임을 망치거나 오류가 날 수 있으므로 편집하지 마십시오.
버그가 있으면 수정하지 마시고 게임 토론이나 해당 개발자의 사용자 토론에 알려주세요.
GRAC All Square.svg
이 게임은 자체 등급 심의를 바탕으로 모든 사용자가 이용할 것을 권장합니다.
등급 지정일: 2024년 10월 27일
/*
	--[[사용자:BANIP|BANIP]] ([[사용자토론:BANIP|토론]]) 2023년 8월 12일 (토) 03:30 (KST)
*/
const TIME_LIMIT = 60
const TIME_ADDITONAL = 20
const RATINGS = [
	{icon:"battery_1_bar",message:"가능성만 있어요.",limit:100},
	{icon:"battery_2_bar",message:"익히셨군요.",limit:200},
	{icon:"battery_3_bar",message:"기대 이상이에요.",limit:300},
	{icon:"battery_4_bar",message:"자랑해도 될 수준이에요.",limit:500},
	{icon:"battery_5_bar",message:"익숙하시군요.",limit:700},
	{icon:"battery_6_bar",message:"완벽에 가까워요.",limit:1000},
	{icon:"battery_full",message:"더이상 무엇을 바라겠어요?",limit:Infinity},
]
const MUSICS = [
	{pagename:"파일:Popcandy goorogi.mp3", step:0},
	{pagename:"파일:Cyrf pluto.mp3", step:40},
	{pagename:"파일:Isao blaze.mp3", step:80},
]


async function main() {
	console.log('color dodge 1.0v')
	await sys.init();

	while(true){
		bgm.stop();
		await pages.lobby();
		let {score} = await pages.game();
		await pages.result({score});
	}
}

const sys = {
	init: async function() {
		$(".vector-sitenotice-container").hide();
		let $body = $("#mw-content-text");
		let bodyMinHeight = window.innerHeight - $body.offset().top - 50;
		$("#mw-content-text").addClass("gamebody")

		$body.css({
			"height": bodyMinHeight,
			"display": "flex",
			"flex-direction": "column",
			"justify-content": "center",
			"position": "relative",
		})
		$body.html('');
	},
	message: async function({$body, msg, ms, speed = 200} = {}) {
		let $msg = $("{div}{/div}".parseHtml()).text(msg).css({display:"none", textAlign:"center",fontSize:"1.5em"});
		$body.append($msg);
		$msg.slideDown(speed/100);
		await util.asleep(ms + speed);
		$msg.slideUp(speed/100);
		await util.asleep(speed);
		$msg.remove();
	},
	initSound: (name) => {
		let thisSound = null
		repo.getFileUrl(`파일:${name}`).then(({url}) => thisSound = new Audio(url));
		return () => {
			if(thisSound) thisSound.cloneNode().play();
		}
	},
	saveRanking: async function({score}) {
		let name = mw.user.getName() || ("익명" + Math.floor(Math.random()*100000).toString().padStart(5,'0'))
		let rankingPagename = mw.config.get('wgPageName') + "/랭킹";
		let content = await repo.getPage({pagename:rankingPagename}); 

		let regex = /'{3}([^']+)'{3}[^\d]+(\d+)/
		let prevRank = (content || "") === "" ?
			[] :
			content.trim().split("\n").filter(v => v.match(regex)).map( row => {
				let [_,name,score]  = row.match(regex)
				return [name,score]
			})

		
		let userPrevRank = prevRank.find(([prevName,_]) => prevName === name);
		let userPrevScore = userPrevRank ? parseInt(userPrevRank[1]) : 0;
		
		if(score <= userPrevScore) {
			mw.notify('이전 점수보다 높지 않아 랭킹에 등록되지 않았습니다. (이전 점수: ' + userPrevScore + '점)');
			return;
		}
		let rankIndex = prevRank.findIndex(([name,thisScore]) => score > thisScore);
		
		let rank = rankIndex === -1 ? prevRank.length + 1 : rankIndex + 1;
		
		let newRank = Object.entries(Object.fromEntries(prevRank.concat([ [name,score] ]))).sort((a,b) => b[1] - a[1]);
		let bold = `'`.repeat(3);
		let newRankText = newRank.map(([name,score]) => `* ${bold}${name}${bold} : ${score}점`).join("\n");

		let success = await repo.editPage({
			pagename: rankingPagename,
			text: newRankText,
			summary: `랭킹 등록(${score}점, ${rank}위)`,
		});
		
		mw.notify(success ? `${name}님의 점수가 등록되었습니다. (랭킹 ${rank}위)` : '랭킹 등록에 실패했습니다.'); 
		return rank;
	}
}

const repo = {
	api: new mw.Api(),
	getFileUrl: async function (pagename) {
		let response = await repo.api.get({
			action: 'query',
			titles: pagename,
			prop: 'imageinfo',
			iiprop: 'url',
			formatversion: 2
		});
		let pages = response.query.pages;
		let url = pages[0].imageinfo[0].url;
		return { url };
	},
	editPage: async function({pagename, text, summary, bot= true, minor = true} = {}) {

		let response = await repo.api.post({
			action: 'edit', title: pagename,
			text, summary, bot, minor, 
			token: await repo.api.getToken('csrf')
		});

		if (response?.edit?.result === 'Success') {
			return true 
		} else { 
			return false;
		}

	},
	getPage: async function({pagename}){
		let response = await repo.api.get({
			action: 'query',
			prop: 'revisions',
			rvprop: 'content',
			titles: pagename,
			formatversion: 2,
		});
		if(response.query === undefined) return ''
		let pages = response.query.pages;
		let content = pages[0].revisions[0].content;

		return content
	}
}
const sound = {
	intro: sys.initSound("dodge_intro.mp3"),
	result: sys.initSound("dodge_result.mp3"),
	move: sys.initSound("dodge_move.mp3"),
	success: sys.initSound("dodge_success.mp3"),
	count: sys.initSound("dodge_count.mp3"),
	start: sys.initSound("dodge_start.mp3"),
}


String.prototype.parseHtml = function(){
	return this.replace(/{/g, "<").replace(/}/g, ">");
}
const util = {
	asleep: async (ms) => {
		return new Promise(resolve => setTimeout(resolve, ms));
	},
}

const pages = {
	$body: $("#mw-content-text"),
	lobby: () => {
		return new Promise(resolve => {
			pages.$body.html(`
				{div class="game lobby"}
					{div class="title"}{span class="skyblue"}컬러{/span} {span class="rose"}닷지{/span}{/div} 
					{div class="btn start"}시작하기{/div}
				{/div}
			`.parseHtml());
			//debugger
			let childSelectors = ".skyblue, .rose, .start";
			pages.$body.find(childSelectors).css({display:"none"});
			for( let [i, el] of Object.entries(pages.$body.find(childSelectors).toArray())){
				if(!el) continue;
				setTimeout(() => {
					$(el).slideDown(500);
				}, i*500);
			}

			pages.$body.find(".btn").on("click", async () => {
				sound.move();
				pages.$body.find(childSelectors).fadeOut(500);
				await util.asleep(500);
				resolve();
			});
		});
	},
	game: () => {
		let $body = pages.$body;
		$body.html(`
			{div class="game play"}
				{div class="bg"}{/div}
				{div class="timer-wrapper"}{/div}
				{div class="grid-wrapper"}{/div}
			{/div}
		`.parseHtml());
		let gridGen = function*(){
			let inits = [0,1,4,2]
			let repeat = [5,4,2,Infinity]
			let now = [0,0,0,0]
			while(true){
				let getValue = (i) => now[i] + inits[i]
				for(let i = 0; i < now.length; i++){
					if(now[i] >= repeat[i]){
						now[i] = 0; now[i+1] += 1;inits[i] += 1;
					}
				}
				yield new Grid({shuffleCount:getValue(1), row:getValue(2), col:getValue(2), depth:getValue(3)})
				now[0] += 1;

			}
		}

		let gridIter = gridGen();
		let timer = new Timer(TIME_LIMIT)
		let grid = gridIter.next().value
		$body.find(".timer-wrapper").html(timer.getElement());
		$body.find(".grid-wrapper").html(grid.getElement());

		let $bg = $body.find(" .game > .bg");
		return new Promise(async resolve => {
			sound.intro();
			await sys.message({$body:$bg, msg:"모두 같은색으로 만드세요.", ms:3000});
			for(let i = 3; i > 0; i--){
				sound.count();
				await sys.message({$body:$bg, msg:i, ms:500});
			}
			sound.start();
			bgm.play();
			$bg.fadeOut(200);
			
			(async function(){
				let index = 0;
				while(++index){
					await grid.wait();
					sound.success();
					timer.addTime(TIME_ADDITONAL);
					grid = gridIter.next().value
					$body.find(".grid-wrapper").html(grid.getElement());

					let musicIndex = MUSICS.findIndex(({step}) => step === index);
					if(musicIndex !== -1){
						bgm.play(musicIndex);
					}
				}
			})();
			timer.start();
			await timer.wait();
			let score = timer.getScore();
			resolve({score});
		});
	},
	result: ({score}) => {
		sound.result();

		let {icon, message} = RATINGS.find(({limit}) => score < limit);

		pages.$body.html(`
			{div class="game result"}
			{div class="title"}{span class="rose"}게임 결과{/span}{/div} 
				{div class="score"}${score.toFixed(0).toLocaleString()}점{/div}
				{div class="rating"}
					{span class="material-symbols-outlined icon"}${icon}{/span}
					{span class="message"}${message}{/span}
				{/div}
				{div class="btn-wrapper"}
					{div class="btn restart"}다시하기{/div}
					{div class="btn totalk"}토론{/div}
					{div class="btn rank"}랭킹등록{/div}
				{/div}
			{/div}
		`.parseHtml());
		
		return new Promise(resolve => {
			
			pages.$body.find(".totalk").on("click", () => {
				let titleObj = new mw.Title(mw.config.get('wgPageName'));
				let talkPageUrl = titleObj.getTalkPage().getUrl();
				location.href = talkPageUrl;
			});
			pages.$body.find(".restart").on("click", () => {
				resolve();
			});
			pages.$body.find(".rank").on("click", async function(){
				$(this).off("click").text("등록중...");
				await sys.saveRanking({score});
				$(this).text("등록완료");
			});
		});
	},
}


const bgm = {
	audioEl: null,
	play: async function(level = 0) {
		let {url} = await repo.getFileUrl(MUSICS[level].pagename); 
		bgm.stop();
		this.audioEl = new Audio(url);
		this.currentTime = 0;
		this.audioEl.addEventListener('ended', function() {
			this.currentTime = 0;
			this.play();
		}, false);

		this.audioEl.play();
	},
	stop: function() {
		if (this.audioEl) {
			this.audioEl.pause();
		}
	}
}


class Timer{
	constructor(limit){

		this._limit = limit;
		this._time = 0;
		this._score = 0;
		this._$element = $(`{div class="timer"}{div class="bg"}{/div}{div class="remain"}${this._limit}{/div}{/div}`.parseHtml());
		this._resolve = () => {};
	}
	start(){
		let $remain = this._$element.find(".remain");
		let $bg = this._$element.find(".bg");
		this._time = this._limit;
		this._score = 0;
		$remain.text(this._time);
		this._interval = setInterval(() => {
			this._time = Math.floor((this._time - 0.1) * 10) / 10;
			$remain.text(this._time);
			if(this._time <= 0){
				clearInterval(this._interval);
				this._resolve();
			}
			
			// time과 limt에 따라 $element의 width를 조정
			let width = this._time / this._limit * 100;
			$bg.css({
				width: `${width}%`,
				background: `hsl(${width * 1.2}, 100%, 80%)`
			});

		}, 100);
	}

	addTime(amount){
		this._time += amount;
		let diff = this._time - this._limit;
		this._score += diff;
		if(this._time > this._limit) this._time = this._limit;
	}
	wait(){
		return new Promise(resolve => {
			this._resolve = resolve;
		});

	}
	getScore(){
		return Math.floor(this._score);
	}
	getElement(){
		return this._$element;
	}
}

class Grid{
	static DIRECTIONS = [ [1,1],[1,0],[1,-1],[0,1],[0,0],[0,-1],[-1,1],[-1,0],[-1,-1] ]
	constructor({row=5, col=5, depth=2, shuffleCount=2}={}){
		this._row = row; this._col = col; this._depth = depth; this._resolve = () => {}
		this._grid = Array.from({length:row},()=>Array.from({length:col},()=>0));
		this._shuffle(this._grid,shuffleCount);
	}

	static _isMobile(){
		return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
	}

	_sample(prevArr, count=1){
		let result = [];
		let arr = [...prevArr];
		for(let i=0;i<count;i++){
			let index = Math.floor(Math.random()*arr.length);
			result.push(arr[index]);
			arr.splice(index,1);
		}
		return result;
	}

	_shuffle(grid,count){
		let size = this._row * this._col;
		let depth = this._depth;
		let rawCells = Array.from({length:size},(_,i)=>i)
		rawCells = Array.from({length:depth - 1}, ()=>rawCells).flat();

		let cells = this._sample(rawCells, count);
		
		for(let cell of cells){
			let row = Math.floor(cell/this._col);
			let col = cell%this._col;
			this._move(grid,row,col);
		}
	}

	_move(grid,row,col){
		for(let [r,c] of Grid.DIRECTIONS){
			let value = grid?.[row+r]?.[col+c];
			if(value !== undefined){
				grid[row+r][col+c] = ((grid[row+r][col+c] || 0) + 1) % this._depth;
			}
		}
	}

	_validate(grid){
		return grid.every(row=>row.every(col=>col === grid[0][0]));
	}

	getElement(){
		let $grid = $("{div class='grid'}{/div}".parseHtml()).css({
			display:"grid", width: "100%", height: "100%", gap: "8px",
			gridTemplateColumns: `repeat(${this._col},1fr)`, gridTemplateRows: `repeat(${this._row},1fr)`,
		});
		let getColor = (row,col) => `hsl(${this._grid[row][col]*360/this._depth},100%,80%)`;
		for(let i = 0; i < this._row; i++){
			for(let j = 0; j < this._col; j++){
				let $div = $("{div class='cell'}{/div}".parseHtml()).css({
					backgroundColor: getColor(i,j),
				}).attr({row:i,col:j});
				$grid.append($div);
			}
		}

		$grid.find(".cell").on(Grid._isMobile()?"touchstart":"mousedown",e => {
			let $cell = $(e.currentTarget);
			let [row,col] = [$cell.attr("row"),$cell.attr("col")].map(v=>+v);
			this._move(this._grid,row,col);
			if(this._validate(this._grid)) this._resolve();
			sound.move();
			
			$cell.css({backgroundColor:getColor(row,col)}); // 현재 셀 색상 변경
			setTimeout(()=>{ // 주변 셀 색상 변경
				for(let [r,c] of Grid.DIRECTIONS){
					let $cell = $grid.find(`.cell[row=${row+r}][col=${col+c}]`);
					if($cell.length) $cell.css({backgroundColor:getColor(row+r,col+c)});
				}
			},100);
		});


		return $grid;
	}

	wait(){
		return new Promise(resolve=>{
			this._resolve=resolve
		});
	}
}

$(main);

ios는 지원되지 않습니다.