diff --git a/assets/audio/bridgebgm.mp3 b/assets/audio/bridgebgm.mp3 new file mode 100644 index 0000000..ff4a006 Binary files /dev/null and b/assets/audio/bridgebgm.mp3 differ diff --git a/assets/audio/classroombgm.mp3 b/assets/audio/classroombgm.mp3 new file mode 100644 index 0000000..25408de Binary files /dev/null and b/assets/audio/classroombgm.mp3 differ diff --git a/assets/audio/culturelandbgm.mp3 b/assets/audio/culturelandbgm.mp3 new file mode 100644 index 0000000..f5c0f9b Binary files /dev/null and b/assets/audio/culturelandbgm.mp3 differ diff --git a/assets/audio/parkbgm.mp3 b/assets/audio/parkbgm.mp3 new file mode 100644 index 0000000..f1ddd5a Binary files /dev/null and b/assets/audio/parkbgm.mp3 differ diff --git a/assets/boy1.png b/assets/boy1.png new file mode 100644 index 0000000..cb8b45b Binary files /dev/null and b/assets/boy1.png differ diff --git a/assets/boy2.png b/assets/boy2.png new file mode 100644 index 0000000..344225d Binary files /dev/null and b/assets/boy2.png differ diff --git a/assets/boy3.png b/assets/boy3.png new file mode 100644 index 0000000..90222ff Binary files /dev/null and b/assets/boy3.png differ diff --git a/assets/bridge1.png b/assets/bridge1.png new file mode 100644 index 0000000..ca8863b Binary files /dev/null and b/assets/bridge1.png differ diff --git a/assets/bridge2.png b/assets/bridge2.png new file mode 100644 index 0000000..bc97755 Binary files /dev/null and b/assets/bridge2.png differ diff --git a/assets/bridge3.png b/assets/bridge3.png new file mode 100644 index 0000000..0a66ba4 Binary files /dev/null and b/assets/bridge3.png differ diff --git a/assets/chatbox.png b/assets/chatbox.png new file mode 100644 index 0000000..0e49776 Binary files /dev/null and b/assets/chatbox.png differ diff --git a/assets/classroom.png b/assets/classroom.png new file mode 100644 index 0000000..6c824f6 Binary files /dev/null and b/assets/classroom.png differ diff --git a/assets/cultureland.png b/assets/cultureland.png new file mode 100644 index 0000000..0df28cc Binary files /dev/null and b/assets/cultureland.png differ diff --git a/assets/girl1.png b/assets/girl1.png new file mode 100644 index 0000000..aa25791 Binary files /dev/null and b/assets/girl1.png differ diff --git a/assets/girl2.png b/assets/girl2.png new file mode 100644 index 0000000..d7d52d1 Binary files /dev/null and b/assets/girl2.png differ diff --git a/assets/girl3.png b/assets/girl3.png new file mode 100644 index 0000000..f58b66d Binary files /dev/null and b/assets/girl3.png differ diff --git a/assets/invisable.png b/assets/invisable.png new file mode 100644 index 0000000..1914264 Binary files /dev/null and b/assets/invisable.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..7ecbe3c Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/main.jpg b/assets/main.jpg new file mode 100644 index 0000000..ec56a40 Binary files /dev/null and b/assets/main.jpg differ diff --git a/assets/nicknamebg.png b/assets/nicknamebg.png new file mode 100644 index 0000000..6f7213b Binary files /dev/null and b/assets/nicknamebg.png differ diff --git a/assets/park.png b/assets/park.png new file mode 100644 index 0000000..2892dc0 Binary files /dev/null and b/assets/park.png differ diff --git a/assets/start_button.png b/assets/start_button.png new file mode 100644 index 0000000..5330f1c Binary files /dev/null and b/assets/start_button.png differ diff --git a/assets/worldbg.png b/assets/worldbg.png new file mode 100644 index 0000000..f23eaa5 Binary files /dev/null and b/assets/worldbg.png differ diff --git a/bridgescene.js b/bridgescene.js new file mode 100644 index 0000000..3730b16 --- /dev/null +++ b/bridgescene.js @@ -0,0 +1,488 @@ +import { stompClient } from './game.js'; + +const SERVER_URL = 'https://kuriverse.shop'; + +class BridgeScene extends Phaser.Scene { + constructor() { + super('BridgeScene'); + this.currentDirection = null; + this.moveInterval = null; + this.otherPlayers = new Map(); + this.bodytypeMap = new Map(); + this.activeBubble = null; + this.activePortal = null; + this.chatInputField = null; + this.customizationSub = null; + this.positionsSub = null; + this.chatSub = null; + } + + init(data) { + if (data && data.userInfo) { + window.userInfo = data.userInfo; + } + this.roomId = window.userInfo?.roomId || 4; + this.character = window.userInfo?.character || 'boy1'; + this.nickname = window.userInfo?.nickname || '사용자'; + } + + preload() { + this.load.audio('bridgeBgm', 'assets/audio/bridgebgm.mp3'); + const characterList = ['boy1', 'boy2', 'boy3', 'girl1', 'girl2', 'girl3']; + characterList.forEach(key => { + if (!this.textures.exists(key)) this.load.image(key, `assets/${key}.png`); + }); + + const bgMap = { 4: 'bridge3', 5: 'bridge2', 6: 'bridge1' }; + const bgKey = bgMap[this.roomId] || 'bridge1'; + if (!this.textures.exists(bgKey)) { + this.load.image(bgKey, `assets/${bgKey}.png`); + } + this.bgKeyToUse = bgKey; + } + + isStompConnected() { + return stompClient && stompClient.active; + } + + create() { + this.bgm = this.sound.add('bridgeBgm', { loop: true, volume: 0.1 }); + this.bgm.play(); + this.input.keyboard.enabled = true; + this.otherPlayers.clear(); + this.bodytypeMap.clear(); + this.roomNameMap = { 4: "통로 1", 5: "통로 2", 6: "통로 3" }; + this.currentRoomName = this.roomNameMap[this.roomId] || "알 수 없는 통로"; + + this.events.on('shutdown', this.shutdown, this); + + this.input.keyboard.on('keydown', this.handleKeyDown, this); + this.input.keyboard.on('keyup', this.handleKeyUp, this); + + this.add.image(800, 450, this.bgKeyToUse).setDisplaySize(1600, 900).setDepth(0); + const titleText = this.currentRoomName; + this.add.rectangle(800, 50, 300, 60, 0xB593CC).setDepth(5).setStrokeStyle(2, 0xffffff); + this.add.text(800, 50, titleText, { fontSize: '32px', fontFamily: 'Pretendard', color: '#ffffff' }).setOrigin(0.5).setDepth(6); + this.add.rectangle(300, 750, 580, 200, 0x000000, 0.4).setDepth(2); + + this.chatLogContainer = this.add.dom(300, 730).createFromHTML(` +
+ +
+
+`).setOrigin(0.5).setDepth(6); + + this.chatInput = this.add.dom(300, 850).createFromHTML(` +
+ + +
+`).setOrigin(0.5).setDepth(10); + + this.chatInput.on('create', (dom) => { + this.chatInputField = dom.getChildByID('chat-message'); + const sendButton = dom.getChildByID('send-btn'); + const sendMessage = () => { + const message = this.chatInputField.value.trim(); + if (message) { + this.addChatLog(`${this.nickname}: ${message}`); + this.chatInputField.value = ''; + this.createBubble(message); + } + this.chatInputField.blur(); + }; + + this.chatInputField.addEventListener('focus', () => this.input.keyboard.enabled = false); + this.chatInputField.addEventListener('blur', () => this.input.keyboard.enabled = true); + this.chatInputField.addEventListener('keydown', (event) => { + if (event.key === 'Enter') sendMessage(); + }); + sendButton.addEventListener('click', sendMessage); + }); + + this.addChatLog = (text) => { + const chatBox = this.chatLogContainer.getChildByID('chat-log-box'); + if (chatBox) { + const p = document.createElement('p'); + p.textContent = text; + p.style.margin = '0 0 6px 0'; + chatBox.appendChild(p); + chatBox.scrollTop = chatBox.scrollHeight; + } + }; + + // 채팅 입력 이벤트 처리 + this.time.delayedCall(100, () => { + const chatInputField = this.chatInput.getChildByID('chat-message'); + const sendBtn = this.chatInput.getChildByID('send-btn'); + // 채팅 전송 함수 + const sendMessage = () => { + const message = chatInputField.value.trim(); + if (message && this.isStompConnected()) { + stompClient.publish({ + destination: '/app/chat.send', + body: JSON.stringify({ + roomId: this.roomId, + nickname: this.nickname, + content: message + }) + }); + chatInputField.value = ''; + } else if (!this.isStompConnected()) { + this.addChatLog('[시스템] 연결이 끊어졌습니다. 잠시 후 다시 시도해주세요.'); + } + if (chatInputField.value.trim() === '') { + setTimeout(() => { + chatInputField.blur(); + }, 10); // 약간의 딜레이를 주어 blur가 정상 동작하도록 + } + }; + // 엔터키 전송 + chatInputField.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + sendMessage(); + } + }); + // 버튼 클릭 전송 + sendBtn.addEventListener('click', sendMessage); + + // 입력창 포커스/블러에 따라 게임 키 입력 활성/비활성 + chatInputField.addEventListener('focus', () => this.input.keyboard.enabled = false); + chatInputField.addEventListener('blur', () => this.input.keyboard.enabled = true); + }); + + this.player = this.physics.add.sprite(800, 650, this.character).setDisplaySize(100, 120).setCollideWorldBounds(true).setOrigin(0.5); + this.nicknameBg = this.add.rectangle(this.player.x, this.player.y + 78, 100, 22, 0x000000, 0.4).setOrigin(0.5).setDepth(5); + this.nicknameText = this.add.text(this.player.x, this.player.y + 78, this.nickname, { font: '14px Pretendard', fill: '#ffffff' }).setOrigin(0.5).setDepth(6); + + // 포털 이동키 + this.eKey = this.input.keyboard.addKey('E'); + this.createPortals(this.roomId); + + window.addEventListener('beforeunload', () => { + if (stompClient.active) { + this.leaveRoom(this.roomId, this.nickname); + stompClient.deactivate(); + } + // 로그아웃 API 호출 + navigator.sendBeacon('/api/users/logout'); + }); + + this.initWebSocket(this.roomId, this.nickname); + + const characterList = ['boy1', 'boy2', 'boy3', 'girl1', 'girl2', 'girl3']; + const npcCount = Phaser.Math.Between(5, 6); + + for (let i = 0; i < npcCount; i++) { + const key = Phaser.Utils.Array.GetRandom(characterList); + const x = Phaser.Math.Between(100, 1500); + const y = Phaser.Math.Between(600, 880); + + const npc = this.add.sprite(x, y, key) + .setDisplaySize(100, 120) + .setDepth(1); + npc.setFlipX(Math.random() < 0.5); // 랜덤 방향 + } + } + + async joinRoom(roomId, nickname) { + try { + await fetch(`${SERVER_URL}/api/rooms/${roomId}/join`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname }) + }); + console.log('방 입장 성공'); + } catch (e) { + console.error('방 입장 오류:', e); + } + } + + async leaveRoom(roomId, nickname) { + try { + await fetch(`${SERVER_URL}/api/rooms/${roomId}/leave`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname: nickname }) + }); + console.log("방 퇴장 완료"); + } catch (error) { + console.error("퇴장 오류:", error); + } + } + + async getAllCustomizations() { + try { + const response = await fetch(`${SERVER_URL}/api/customization/all`); + if (!response.ok) { + throw new Error(`전체 외형 정보 로딩 실패: ${response.status}`); + } + const allCustoms = await response.json(); + allCustoms.forEach(custom => { + const bodytype = custom.bodytype || custom.bodyType; + this.bodytypeMap.set(custom.nickname, bodytype); + }); + } catch (e) { + console.error('전체 외형 정보 로딩 중 오류:', e); + } + } + + async initWebSocket(roomId, nickname) { + await this.joinRoom(roomId, nickname); + await this.getAllCustomizations(); + + if (stompClient.active) { + this.setupSubscriptions(); + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname, direction: "init", roomId, x: 800 / 40, y: 650 / 40 }) }); + } else { + stompClient.onConnect = () => { + this.setupSubscriptions(); + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname, direction: "init", roomId, x: 800 / 40, y: 650 / 40 }) }); + }; + } + } + + setupSubscriptions() { + const bodytypeToCharacter = { 1: 'boy1', 2: 'boy2', 3: 'boy3', 4: 'girl1', 5: 'girl2', 6: 'girl3' }; + + if (this.chatSub) this.chatSub.unsubscribe(); + if (this.positionsSub) this.positionsSub.unsubscribe(); + if (this.customizationSub) this.customizationSub.unsubscribe(); + + this.customizationSub = stompClient.subscribe('/topic/customization', (message) => { + const data = JSON.parse(message.body); + const bodytype = data.bodytype || data.bodyType; + this.bodytypeMap.set(data.nickname, bodytype); + if (this.otherPlayers.has(data.nickname)) { + const player = this.otherPlayers.get(data.nickname); + const charKey = bodytypeToCharacter[bodytype] || 'boy1'; + player.sprite.setTexture(charKey); + } + }); + + this.positionsSub = stompClient.subscribe('/topic/positions', (message) => { + const positions = JSON.parse(message.body); + const usersInSameRoom = positions.filter(pos => pos.roomName === this.currentRoomName); + const receivedNicknames = new Set(usersInSameRoom.map(p => p.nickname)); + const myData = positions.find(pos => pos.nickname === this.nickname); + if (myData && this.player) { + const SCALE = 16; + this.player.setPosition(myData.x * SCALE, myData.y * SCALE); + } + usersInSameRoom.forEach(pos => { + if (pos.nickname === this.nickname) return; + const SCALE = 16; + const targetX = pos.x * SCALE; + const targetY = pos.y * SCALE; + if (!this.otherPlayers.has(pos.nickname)) { + const bodytype = this.bodytypeMap.get(pos.nickname); + const charKey = bodytypeToCharacter[bodytype] || 'boy1'; + const otherSprite = this.physics.add.sprite(targetX, targetY, charKey).setDisplaySize(100, 120); + const nicknameBg = this.add.rectangle(targetX, targetY + 78, 100, 22, 0x000000, 0.4).setOrigin(0.5).setDepth(5); + const nicknameText = this.add.text(targetX, targetY + 78, pos.nickname, { font: '14px Pretendard', fill: '#ffffff' }).setOrigin(0.5).setDepth(6); + this.otherPlayers.set(pos.nickname, { sprite: otherSprite, nicknameBg, nicknameText, chatBubble: null }); + } else { + const playerObj = this.otherPlayers.get(pos.nickname); + playerObj.sprite.setPosition(targetX, targetY); + } + }); + this.otherPlayers.forEach((playerData, nick) => { + if (!receivedNicknames.has(nick)) { + playerData.sprite.destroy(); playerData.nicknameBg.destroy(); playerData.nicknameText.destroy(); + this.otherPlayers.delete(nick); this.bodytypeMap.delete(nick); + } + }); + }); + + stompClient.subscribe(`/queue/warnings/${this.nickname}`, (msg) => { + this.addChatLog(`[⚠️ 경고] ${msg.body}`); + }); + + // 에러 메시지 구독 추가 + stompClient.subscribe(`/queue/errors/${this.nickname}`, (msg) => { + this.addChatLog(`[❌ 에러] ${msg.body}`); + }); + + this.chatSub = stompClient.subscribe(`/topic/room/${this.roomId}`, (msg) => { + let chat; + try { + chat = JSON.parse(msg.body); + } catch (e) { + chat = { nickname: '시스템', content: msg.body }; + } + const sender = chat.nickname; + const content = chat.content; + this.addChatLog(`[${sender}] ${content}`); + // 자신의 말풍선 표시 + if (sender === this.nickname) { + this.showChatBubble(this.player, content, true); + } + // 타 플레이어의 말풍선 표시 + else if (this.otherPlayers.has(sender)) { + this.showChatBubble(this.otherPlayers.get(sender).sprite, content, false, sender); + } + }); + } + + showChatBubble(targetSprite, message, isMe, nickname = null) { + // 자신의 말풍선 or 타 플레이어 말풍선이 이미 있으면 제거 + if (isMe && this.activeBubble) { + this.activeBubble.destroy(); + this.activeBubble = null; + } + if (!isMe && nickname && this.otherPlayers.get(nickname)?.chatBubble) { + this.otherPlayers.get(nickname).chatBubble.destroy(); + this.otherPlayers.get(nickname).chatBubble = null; + } + + const bubbleWidth = 220; + const bubbleHeight = 90; + const tailSize = 12; + + // 말풍선 박스 + const bubbleRect = this.add.graphics(); + bubbleRect.fillStyle(0xffffff, 1); + bubbleRect.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 10); + + // 말풍선 꼬리 + const tail = this.add.graphics(); + tail.fillStyle(0xffffff, 1); + tail.beginPath(); + tail.moveTo(bubbleWidth / 2 - tailSize, bubbleHeight); + tail.lineTo(bubbleWidth / 2, bubbleHeight + tailSize); + tail.lineTo(bubbleWidth / 2 + tailSize, bubbleHeight); + tail.closePath(); + tail.fillPath(); + + // 메시지 텍스트 + const msgText = this.add.text(bubbleWidth / 2, bubbleHeight / 2, message, { + font: '14px Pretendard', + color: '#000000', + align: 'center', + wordWrap: { width: bubbleWidth - 20 } + }).setOrigin(0.5); + + // 말풍선 컨테이너 (타겟 스프라이트 기준 위치) + const bubble = this.add.container( + targetSprite.x - bubbleWidth / 2, + targetSprite.y - 150, + [bubbleRect, tail, msgText] + ).setDepth(6); + + // 3초 후 말풍선 제거 + this.time.delayedCall(3000, () => { + bubble.destroy(); + if (isMe) this.activeBubble = null; + else if (nickname && this.otherPlayers.get(nickname)) { + this.otherPlayers.get(nickname).chatBubble = null; + } + }); + + // 자신 or 타 플레이어 말풍선 저장 + if (isMe) this.activeBubble = bubble; + else if (nickname && this.otherPlayers.get(nickname)) { + this.otherPlayers.get(nickname).chatBubble = bubble; + } + } + + createPortals(roomId) { + const portalConfig = { + 4: [{ x: 100, y: 670, target: { scene: 'MainScene', roomId: 1 } }, { x: 1500, y: 670, target: { scene: 'MainScene', roomId: 3 } }], + 5: [{ x: 100, y: 640, target: { scene: 'MainScene', roomId: 2 } }, { x: 1500, y: 640, target: { scene: 'MainScene', roomId: 1 } }], + 6: [{ x: 100, y: 640, target: { scene: 'MainScene', roomId: 3 } }, { x: 1500, y: 640, target: { scene: 'MainScene', roomId: 2 } }] + }; + if (!portalConfig[roomId]) return; + + this.portals = this.physics.add.staticGroup(); + portalConfig[roomId].forEach(portalInfo => { + const p = this.add.circle(portalInfo.x, portalInfo.y, 30, 0xFF00FF, 0.3).setDepth(4); + this.portals.add(p); + p.setData('target', portalInfo.target); + }); + } + + update() { + if (!this.player) return; + + this.activePortal = null; + this.physics.world.overlap(this.player, this.portals, (player, portal) => { + this.activePortal = portal.getData('target'); + }, null, this); + + if (this.activeBubble) this.activeBubble.setPosition(this.player.x - 110, this.player.y - 150); + + if (this.nicknameText && this.nicknameBg) { + this.nicknameText.setPosition(this.player.x, this.player.y + 78); + this.nicknameBg.setPosition(this.player.x, this.player.y + 78); + } + + this.otherPlayers.forEach(playerObj => { + const { sprite, nicknameBg, nicknameText, chatBubble } = playerObj; + nicknameBg.setPosition(sprite.x, sprite.y + 78); + nicknameText.setPosition(sprite.x, sprite.y + 78); + if (chatBubble) chatBubble.setPosition(sprite.x - 110, sprite.y - 150); + }); + + if (this.activePortal && Phaser.Input.Keyboard.JustDown(this.eKey)) { + this.input.keyboard.enabled = false; + if (this.bgm) { + this.bgm.stop(); + this.bgm.destroy(); + this.bgm = null; + } + this.leaveRoom(this.roomId, this.nickname).finally(() => { + window.userInfo.roomId = this.activePortal.roomId; + this.scene.start(this.activePortal.scene, { userInfo: window.userInfo }); + }); + this.activePortal = null; + } + } + + handleKeyDown(event) { + if (!this.input.keyboard.enabled) return; + const keyMap = { ArrowUp: 'w', ArrowDown: 's', ArrowLeft: 'a', ArrowRight: 'd' }; + const dir = keyMap[event.key] || (['w', 'a', 's', 'd'].includes(event.key.toLowerCase()) ? event.key.toLowerCase() : null); + if (dir && dir !== this.currentDirection) { + this.currentDirection = dir; + if (this.moveInterval) clearInterval(this.moveInterval); + const payload = { nickname: this.nickname, direction: dir, roomId: this.roomId }; + stompClient.publish({ destination: "/app/move", body: JSON.stringify(payload) }); + this.moveInterval = setInterval(() => { + stompClient.publish({ destination: "/app/move", body: JSON.stringify(payload) }); + }, 100); + } + } + + handleKeyUp(event) { + if (!this.input.keyboard.enabled) return; + const dirKeys = ['w', 'a', 's', 'd', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; + if (dirKeys.includes(event.key)) { + if (this.moveInterval) clearInterval(this.moveInterval); + this.moveInterval = null; + this.currentDirection = null; + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname: this.nickname, direction: "stop", roomId: this.roomId }) }); + } + } + + shutdown() { + console.log(`BridgeScene shutdown: Cleaning up...`); + this.input.keyboard.off('keydown', this.handleKeyDown, this); + this.input.keyboard.off('keyup', this.handleKeyUp, this); + if (this.customizationSub) this.customizationSub.unsubscribe(); + if (this.positionsSub) this.positionsSub.unsubscribe(); + if (this.chatSub) this.chatSub.unsubscribe(); + if (this.moveInterval) clearInterval(this.moveInterval); + if (this.bgm) { + this.bgm.stop(); + this.bgm.destroy(); + this.bgm = null; + } + } +} + +export default BridgeScene; \ No newline at end of file diff --git a/characterselectscene.js b/characterselectscene.js new file mode 100644 index 0000000..4cfad46 --- /dev/null +++ b/characterselectscene.js @@ -0,0 +1,92 @@ +const SERVER_URL = 'https://kuriverse.shop'; + +class CharacterSelectScene extends Phaser.Scene { + constructor() { + super('CharacterSelectScene'); + } + + preload() { + this.load.image('character_bg', 'assets/nicknamebg.png'); + this.load.image('boy1', 'assets/boy1.png'); + this.load.image('boy2', 'assets/boy2.png'); + this.load.image('boy3', 'assets/boy3.png'); + this.load.image('girl1', 'assets/girl1.png'); + this.load.image('girl2', 'assets/girl2.png'); + this.load.image('girl3', 'assets/girl3.png'); + } + + create(data) { + const nickname = data?.nickname || window.userInfo?.nickname; + if (!nickname) { + alert('닉네임 인식 실패로 시작화면으로 돌아갑니다.'); + this.scene.start('NicknameScene'); + return; + } + + const characterKeyToBodytype = { + 'boy1': 1, 'boy2': 2, 'boy3': 3, + 'girl1': 4, 'girl2': 5, 'girl3': 6 + }; + + const saveCustomization = async (nickname, characterKey) => { + const bodytypeInt = characterKeyToBodytype[characterKey]; + + try { + const response = await fetch(`${SERVER_URL}/api/customization/update`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ nickname: nickname, bodyType: bodytypeInt }) + }); + if (response.ok) { + console.log("커스터마이징 저장 완료:", `nickname=${nickname}, bodytype=${bodytypeInt}`); + this.scene.start('WorldMapScene', { nickname, character: characterKey }); + } else { + const errorText = await response.text(); + alert(`커스터마이징 저장 실패: ${errorText}`); + } + } catch (error) { + console.error("저장 중 오류:", error); + } + }; + + this.add.image(800, 450, 'character_bg').setDisplaySize(1600, 900); + this.add.rectangle(800, 100, 496, 72, 0xB593CC).setOrigin(0.5).setDepth(1); + this.add.text(800, 100, '캐릭터를 선택해주세요', { + fontFamily: 'Pretendard', + fontSize: '32px', + fontStyle: 'bold', + color: '#ffffff' + }).setOrigin(0.5).setDepth(2); + + const startX = 300; + const gapX = 500; + + const characters = [ + { key: 'boy1', x: startX + 0 * gapX, y: 320 }, + { key: 'boy2', x: startX + 1 * gapX, y: 320 }, + { key: 'boy3', x: startX + 2 * gapX, y: 320 }, + { key: 'girl1', x: startX + 0 * gapX, y: 580 }, + { key: 'girl2', x: startX + 1 * gapX, y: 580 }, + { key: 'girl3', x: startX + 2 * gapX, y: 580 }, + ]; + + characters.forEach(({ key, x, y }) => { + const sprite = this.add.image(x, y, key) + .setDisplaySize(200, 250) + .setInteractive({ useHandCursor: true }) + .setDepth(3); + sprite.on('pointerdown', () => { + this.selectCharacter(key); + saveCustomization(nickname, key); + }); + }); + } + + selectCharacter(characterKey) { + if (!window.userInfo) window.userInfo = {}; + window.userInfo.character = characterKey; + } +} + +export default CharacterSelectScene; \ No newline at end of file diff --git a/game.js b/game.js index aa7d212..21ab5c6 100644 --- a/game.js +++ b/game.js @@ -1,14 +1,59 @@ + +import StartScene from './StartScene.js'; +import NicknameScene from './NicknameScene.js'; +import CharacterSelectScene from './CharacterSelectScene.js'; +import WorldMapScene from './WorldMapScene.js'; +import MainScene from './mainScene.js'; +import BridgeScene from './BridgeScene.js'; + +import { Client } from 'https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.1/+esm'; + const config = { type: Phaser.AUTO, - width: 1600, - height: 900, + //width: 1600, + //height: 900, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 1600, + height: 900, + }, + parent: 'game-container', physics: { default: 'arcade', - arcade: { - debug: false - } + arcade: { debug: false } }, - scene: [LoginScene, MainScene] + dom: { + createContainer: true + }, + scene: [ + StartScene, + NicknameScene, + CharacterSelectScene, + WorldMapScene, + MainScene, + BridgeScene + ] }; -const game = new Phaser.Game(config); \ No newline at end of file +const game = new Phaser.Game(config); + +const socket = new SockJS(WS_URL); + +const stompClient = new Client({ + webSocketFactory: () => socket, + reconnectDelay: 5000, + debug: (str) => console.log('[STOMP]', str) +}); + +stompClient.onConnect = () => { + console.log("STOMP WebSocket 연결 성공"); +}; + +stompClient.onStompError = (frame) => { + console.error("STOMP 오류:", frame.headers['message']); +}; + +stompClient.activate(); + +export { stompClient, socket }; diff --git a/index.html b/index.html index 7640e9f..fd7bdfc 100644 --- a/index.html +++ b/index.html @@ -1,37 +1,34 @@ + Kuriverse + + + + - - - - +
+ - \ No newline at end of file + diff --git a/mainScene.js b/mainScene.js index 1f3e4d4..c60f086 100644 --- a/mainScene.js +++ b/mainScene.js @@ -1,110 +1,473 @@ +import { stompClient } from './game.js'; + +const SERVER_URL = 'https://kuriverse.shop'; + class MainScene extends Phaser.Scene { constructor() { super('MainScene'); + this.currentDirection = null; + this.moveInterval = null; + this.otherPlayers = new Map(); + this.bodytypeMap = new Map(); + this.activeBubble = null; + this.activePortal = null; + this.chatInputField = null; + this.customizationSub = null; + this.positionsSub = null; + this.chatSub = null; + } + + init(data) { + if (data && data.userInfo) { + window.userInfo = data.userInfo; + } + this.roomId = window.userInfo?.roomId || 1; + this.character = window.userInfo?.character || 'boy1'; + this.nickname = window.userInfo?.nickname || '사용자'; + this.roomNameMap = { 1: "교실", 2: "공원", 3: "문화공간" }; + this.currentRoomName = this.roomNameMap[this.roomId] || "교실"; } preload() { - // 배경/에셋 미사용 - // this.load.image('room1_bg', 'assets/room1_bg.png'); - // this.load.image('room2_bg', 'assets/room2_bg.png'); - // this.load.image('chat_bg', 'assets/chat_bg.png'); + const characterList = ['boy1', 'boy2', 'boy3', 'girl1', 'girl2', 'girl3']; + characterList.forEach(key => { + if (!this.textures.exists(key)) this.load.image(key, `assets/${key}.png`); + }); + + const bgMap = { 1: 'classroom', 2: 'park', 3: 'cultureland' }; + const bgKey = bgMap[this.roomId] || 'classroom'; + if (!this.textures.exists(bgKey)) { + this.load.image(bgKey, `assets/${bgKey}.png`); + } + this.bgKeyToUse = bgKey; + + this.load.audio('classroombgm', 'assets/audio/classroombgm.mp3'); + this.load.audio('parkbgm', 'assets/audio/parkbgm.mp3'); + this.load.audio('culturelandbgm', 'assets/audio/culturelandbgm.mp3'); + } + + isStompConnected() { + return stompClient && stompClient.connected; } create() { - const { roomId, nickname, userId } = window.userInfo; - - // 채팅 로그용 반투명 배경 - const chatBg = this.add.graphics(); - chatBg.fillStyle(0x000000, 0.5); - chatBg.fillRoundedRect(20, 640, 300, 180, 10); - chatBg.setScrollFactor(0); - chatBg.setDepth(1); - - this.chatLogs = []; - this.chatLogStartY = 660; - - // 로그 출력 함수 - const addChatLog = (text) => { - const log = this.add.text(40, this.chatLogStartY, text, { - font: '14px Arial', - fill: '#ffffff' - }).setScrollFactor(0).setDepth(2); - - this.chatLogs.push(log); - this.chatLogStartY += 20; - - if (this.chatLogs.length > 8) { - const old = this.chatLogs.shift(); - old.destroy(); - this.chatLogs.forEach((l, i) => l.y = 660 + i * 20); - this.chatLogStartY = 660 + this.chatLogs.length * 20; + this.input.keyboard.enabled = true; + this.otherPlayers.clear(); + this.bodytypeMap.clear(); + this.input.keyboard.on('keydown', this.handleKeyDown, this); + this.input.keyboard.on('keyup', this.handleKeyUp, this); + + const bgmKey = { + 1: 'classroombgm', + 2: 'parkbgm', + 3: 'culturelandbgm' + }[this.roomId] || 'classroombgm'; + + this.bgm = this.sound.add(bgmKey, { loop: true, volume: 0.1 }); + this.bgm.play(); + + + this.add.image(800, 450, this.bgKeyToUse).setDisplaySize(1600, 900).setDepth(0); + const titleText = this.currentRoomName || '알 수 없는 곳'; + this.add.rectangle(800, 50, 300, 60, 0xB593CC).setDepth(5).setStrokeStyle(2, 0xffffff); + this.add.text(800, 50, titleText, { fontSize: '32px', fontFamily: 'Pretendard', color: '#ffffff' }).setOrigin(0.5).setDepth(6); + this.add.rectangle(300, 750, 580, 200, 0x000000, 0.4).setDepth(2); + + // 채팅 로그 DOM + this.chatLogContainer = this.add.dom(300, 730).createFromHTML(` +
+ +
+
+ `).setOrigin(0.5).setDepth(6); + + // 채팅 입력창 DOM + this.chatInput = this.add.dom(300, 850).createFromHTML(` +
+ + +
+ `).setOrigin(0.5).setDepth(10); + + // 채팅 로그 추가 함수 + this.addChatLog = (text) => { + const chatBox = this.chatLogContainer.getChildByID('chat-log-box'); + if (chatBox) { + const p = document.createElement('p'); + p.textContent = text; + p.style.margin = '0 0 6px 0'; + chatBox.appendChild(p); + chatBox.scrollTop = chatBox.scrollHeight; } }; - // 초록 원 텍스처 생성 - const canvas = this.textures.createCanvas('tempPlayer', 50, 50); - const ctx = canvas.getContext(); - ctx.fillStyle = '#00ff00'; - ctx.beginPath(); - ctx.arc(25, 25, 25, 0, Math.PI * 2); - ctx.fill(); - canvas.refresh(); - - const player = this.physics.add.sprite(800, 450, 'tempPlayer'); - player.setOrigin(0.5, 0.5); - player.setDisplaySize(50, 50); - player.setCollideWorldBounds(true); - - const nicknameText = this.add.text(player.x, player.y - 40, nickname, { - font: '16px Arial', - fill: '#ffffff' - }).setOrigin(0.5); - - const cursors = this.input.keyboard.createCursorKeys(); - const input = document.getElementById('chat-input'); - input.style.display = 'block'; - - // 채팅 입력 처리 - input.addEventListener('keydown', async (e) => { - if (e.key === 'Enter' && input.value.trim() !== '') { - const content = input.value.trim(); - input.value = ''; - - try { - await fetch('http://localhost:8080/api/chat/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userId, roomId, nickname, content }) + // 채팅 입력 이벤트 처리 + this.time.delayedCall(100, () => { + const chatInputField = this.chatInput.getChildByID('chat-message'); + const sendBtn = this.chatInput.getChildByID('send-btn'); + // 채팅 전송 함수 + const sendMessage = () => { + const message = chatInputField.value.trim(); + if (message && this.isStompConnected()) { + stompClient.publish({ + destination: '/app/chat.send', + body: JSON.stringify({ + roomId: this.roomId, + nickname: this.nickname, + content: message + }) }); - } catch (err) { - console.warn('서버 없음 (무시 가능)'); + chatInputField.value = ''; + } else if (!this.isStompConnected()) { + this.addChatLog('[시스템] 연결이 끊어졌습니다. 잠시 후 다시 시도해주세요.'); + } + }; + // 엔터키 전송 + chatInputField.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + sendMessage(); + // 입력창이 비어있고, 엔터키가 다시 눌린 경우(즉, 메시지 전송 후 바로 엔터) + if (chatInputField.value.trim() === '') { + chatInputField.blur(); // 포커스 해제 → 게임 키 입력 활성화 + } } + }); + // 버튼 클릭 전송 + sendBtn.addEventListener('click', sendMessage); + + // 입력창 포커스/블러에 따라 게임 키 입력 활성/비활성 + chatInputField.addEventListener('focus', () => this.input.keyboard.enabled = false); + chatInputField.addEventListener('blur', () => this.input.keyboard.enabled = true); + }); + + this.player = this.physics.add.sprite(800, 650, this.character).setDisplaySize(100, 120).setCollideWorldBounds(true).setOrigin(0.5); + this.nicknameBg = this.add.rectangle(this.player.x, this.player.y + 78, 100, 22, 0x000000, 0.4).setOrigin(0.5).setDepth(5); + this.nicknameText = this.add.text(this.player.x, this.player.y + 78, this.nickname, { font: '14px Pretendard', fill: '#ffffff' }).setOrigin(0.5).setDepth(6); - // 말풍선 표시 (내용만) - const bubble = this.add.text(player.x, player.y - 80, content, { - font: '16px Arial', - fill: '#ffff00', - backgroundColor: 'rgba(0,0,0,0.5)', - padding: { x: 8, y: 4 } - }).setOrigin(0.5); - this.time.delayedCall(3000, () => bubble.destroy()); - - // 로그에 닉네임 포함 출력 - addChatLog(`${nickname}: ${content}`); + // 포털 이동키 + this.eKey = this.input.keyboard.addKey('E'); + this.createPortals(this.roomId); + + window.addEventListener('beforeunload', () => { + if (stompClient.active) { + this.leaveRoom(this.roomId, this.nickname); + stompClient.deactivate(); } + // 로그아웃 API 호출 + navigator.sendBeacon('/api/users/logout'); }); - // update - this.update = () => { - const speed = 200; - player.setVelocity(0); + this.initWebSocket(this.roomId, this.nickname); + + const characterList = ['boy1', 'boy2', 'boy3', 'girl1', 'girl2', 'girl3']; + const npcCount = Phaser.Math.Between(5, 6); + + for (let i = 0; i < npcCount; i++) { + const key = Phaser.Utils.Array.GetRandom(characterList); + const x = Phaser.Math.Between(100, 1500); + const y = Phaser.Math.Between(300, 880); - if (cursors.left.isDown) player.setVelocityX(-speed); - else if (cursors.right.isDown) player.setVelocityX(speed); - if (cursors.up.isDown) player.setVelocityY(-speed); - else if (cursors.down.isDown) player.setVelocityY(speed); + const npc = this.add.sprite(x, y, key) + .setDisplaySize(100, 120) + .setDepth(1); + npc.setFlipX(Math.random() < 0.5); // 랜덤 방향 + } + } + + async joinRoom(roomId, nickname) { + try { + await fetch(`${SERVER_URL}/api/rooms/${roomId}/join`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname }) + }); + console.log('방 입장 성공'); + } catch (e) { + console.error('방 입장 오류:', e); + } + } + + async leaveRoom(roomId, nickname) { + try { + await fetch(`${SERVER_URL}/api/rooms/${roomId}/leave`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname }) + }); + console.log("방 퇴장 완료"); + } catch (error) { + console.error("퇴장 오류:", error); + } + } + + async getAllCustomizations() { + try { + const response = await fetch(`${SERVER_URL}/api/customization/all`); + if (!response.ok) { + throw new Error(`전체 외형 정보 로딩 실패: ${response.status}`); + } + const allCustoms = await response.json(); + allCustoms.forEach(custom => { + const bodytype = custom.bodytype || custom.bodyType; + this.bodytypeMap.set(custom.nickname, bodytype); + }); + } catch (e) { + console.error('전체 외형 정보 로딩 중 오류:', e); + } + } - nicknameText.setPosition(player.x, player.y - 40); + async initWebSocket(roomId, nickname) { + await this.joinRoom(roomId, nickname); + await this.getAllCustomizations(); + + if (stompClient.active) { + this.setupSubscriptions(); + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname, direction: "init", roomId }) }); + } else { + stompClient.onConnect = () => { + this.setupSubscriptions(); + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname, direction: "init", roomId }) }); + }; + } + } + + setupSubscriptions() { + const bodytypeToCharacter = { 1: 'boy1', 2: 'boy2', 3: 'boy3', 4: 'girl1', 5: 'girl2', 6: 'girl3' }; + + if (this.chatSub) this.chatSub.unsubscribe(); + if (this.positionsSub) this.positionsSub.unsubscribe(); + if (this.customizationSub) this.customizationSub.unsubscribe(); + + this.customizationSub = stompClient.subscribe('/topic/customization', (message) => { + const data = JSON.parse(message.body); + const bodytype = data.bodytype || data.bodyType; + this.bodytypeMap.set(data.nickname, bodytype); + if (this.otherPlayers.has(data.nickname)) { + const player = this.otherPlayers.get(data.nickname); + const charKey = bodytypeToCharacter[bodytype] || 'boy1'; + player.sprite.setTexture(charKey); + } + }); + + this.positionsSub = stompClient.subscribe('/topic/positions', (message) => { + const positions = JSON.parse(message.body); + const usersInSameRoom = positions.filter(pos => pos.roomName === this.currentRoomName); + const receivedNicknames = new Set(usersInSameRoom.map(p => p.nickname)); + const myData = positions.find(pos => pos.nickname === this.nickname); + if (myData && this.player) { + const SCALE = 16; + this.player.setPosition(myData.x * SCALE, myData.y * SCALE); + //this.player.setPosition(myData.x, myData.y); + } + usersInSameRoom.forEach(pos => { + if (pos.nickname === this.nickname) return; + const SCALE = 16; + const targetX = pos.x * SCALE; + const targetY = pos.y * SCALE; + if (!this.otherPlayers.has(pos.nickname)) { + const bodytype = this.bodytypeMap.get(pos.nickname); + const charKey = bodytypeToCharacter[bodytype] || 'boy1'; + const otherSprite = this.physics.add.sprite(targetX, targetY, charKey).setDisplaySize(100, 120); + const nicknameBg = this.add.rectangle(targetX, targetY + 78, 100, 22, 0x000000, 0.4).setOrigin(0.5).setDepth(5); + const nicknameText = this.add.text(targetX, targetY + 78, pos.nickname, { font: '14px Pretendard', fill: '#ffffff' }).setOrigin(0.5).setDepth(6); + this.otherPlayers.set(pos.nickname, { sprite: otherSprite, nicknameBg, nicknameText, chatBubble: null }); + } else { + const playerObj = this.otherPlayers.get(pos.nickname); + playerObj.sprite.setPosition(targetX, targetY); + } + }); + this.otherPlayers.forEach((playerData, nick) => { + if (!receivedNicknames.has(nick)) { + playerData.sprite.destroy(); + playerData.nicknameBg.destroy(); + playerData.nicknameText.destroy(); + if (playerData.chatBubble) playerData.chatBubble.destroy(); + this.otherPlayers.delete(nick); + this.bodytypeMap.delete(nick); + } + }); + }); + + stompClient.subscribe(`/queue/warnings/${this.nickname}`, (msg) => { + this.addChatLog(`[⚠️ 경고] ${msg.body}`); + }); + + // 에러 메시지 구독 추가 + stompClient.subscribe(`/queue/errors/${this.nickname}`, (msg) => { + this.addChatLog(`[❌ 에러] ${msg.body}`); + }); + + // 채팅 메시지 구독 + this.chatSub = stompClient.subscribe(`/topic/room/${this.roomId}`, (msg) => { + let chat; + try { + chat = JSON.parse(msg.body); + } catch (e) { + chat = { nickname: '시스템', content: msg.body }; + } + const sender = chat.nickname; + const content = chat.content; + this.addChatLog(`[${sender}] ${content}`); + if (sender === this.nickname) { + this.showChatBubble(this.player, content, true); + } else if (this.otherPlayers.has(sender)) { + this.showChatBubble(this.otherPlayers.get(sender).sprite, content, false, sender); + } + }); + } + + showChatBubble(targetSprite, message, isMe, nickname = null) { + if (isMe && this.activeBubble) { + this.activeBubble.destroy(); + this.activeBubble = null; + } + if (!isMe && nickname && this.otherPlayers.get(nickname)?.chatBubble) { + this.otherPlayers.get(nickname).chatBubble.destroy(); + this.otherPlayers.get(nickname).chatBubble = null; + } + + const bubbleWidth = 220; + const bubbleHeight = 90; + const tailSize = 12; + + const bubbleRect = this.add.graphics(); + bubbleRect.fillStyle(0xffffff, 1); + bubbleRect.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 10); + + const tail = this.add.graphics(); + tail.fillStyle(0xffffff, 1); + tail.beginPath(); + tail.moveTo(bubbleWidth / 2 - tailSize, bubbleHeight); + tail.lineTo(bubbleWidth / 2, bubbleHeight + tailSize); + tail.lineTo(bubbleWidth / 2 + tailSize, bubbleHeight); + tail.closePath(); + tail.fillPath(); + + const msgText = this.add.text(bubbleWidth / 2, bubbleHeight / 2, message, { + font: '14px Pretendard', + color: '#000000', + align: 'center', + wordWrap: { width: bubbleWidth - 20 } + }).setOrigin(0.5); + + const bubble = this.add.container( + targetSprite.x - bubbleWidth / 2, + targetSprite.y - 150, + [bubbleRect, tail, msgText] + ).setDepth(6); + + this.time.delayedCall(3000, () => { + bubble.destroy(); + if (isMe) this.activeBubble = null; + else if (nickname && this.otherPlayers.get(nickname)) { + this.otherPlayers.get(nickname).chatBubble = null; + } + }); + + if (isMe) this.activeBubble = bubble; + else if (nickname && this.otherPlayers.get(nickname)) { + this.otherPlayers.get(nickname).chatBubble = bubble; + } + } + + createPortals(roomId) { + const portalConfig = { + 1: [{ x: 100, y: 450, target: { scene: 'BridgeScene', roomId: 5 } }, { x: 1500, y: 450, target: { scene: 'BridgeScene', roomId: 4 } }], + 2: [{ x: 100, y: 450, target: { scene: 'BridgeScene', roomId: 6 } }, { x: 1500, y: 450, target: { scene: 'BridgeScene', roomId: 5 } }], + 3: [{ x: 100, y: 530, target: { scene: 'BridgeScene', roomId: 4 } }, { x: 1500, y: 530, target: { scene: 'BridgeScene', roomId: 6 } }] }; + if (!portalConfig[roomId]) return; + + this.portals = this.physics.add.staticGroup(); + portalConfig[roomId].forEach(portalInfo => { + const p = this.add.circle(portalInfo.x, portalInfo.y, 30, 0xFF00FF, 0.3).setDepth(4); + this.portals.add(p); + p.setData('target', portalInfo.target); + }); } -} \ No newline at end of file + + update() { + if (!this.player) return; + + this.activePortal = null; + this.physics.world.overlap(this.player, this.portals, (player, portal) => { + this.activePortal = portal.getData('target'); + }, null, this); + + if (this.activeBubble) this.activeBubble.setPosition(this.player.x - 110, this.player.y - 150); + + if (this.nicknameText && this.nicknameBg) { + this.nicknameText.setPosition(this.player.x, this.player.y + 78); + this.nicknameBg.setPosition(this.player.x, this.player.y + 78); + } + + this.otherPlayers.forEach(playerObj => { + const { sprite, nicknameBg, nicknameText, chatBubble } = playerObj; + nicknameBg.setPosition(sprite.x, sprite.y + 78); + nicknameText.setPosition(sprite.x, sprite.y + 78); + if (chatBubble) chatBubble.setPosition(sprite.x - 110, sprite.y - 150); + }); + + if (this.activePortal && Phaser.Input.Keyboard.JustDown(this.eKey)) { + this.input.keyboard.enabled = false; + if (this.bgm) { + this.bgm.stop(); + this.bgm.destroy(); + this.bgm = null; + } + this.leaveRoom(this.roomId, this.nickname).finally(() => { + window.userInfo.roomId = this.activePortal.roomId; + this.scene.start(this.activePortal.scene, { userInfo: window.userInfo }); + }); + this.activePortal = null; + } + } + + handleKeyDown(event) { + if (!this.input.keyboard.enabled) return; + const keyMap = { ArrowUp: 'w', ArrowDown: 's', ArrowLeft: 'a', ArrowRight: 'd' }; + const dir = keyMap[event.key] || (['w', 'a', 's', 'd'].includes(event.key.toLowerCase()) ? event.key.toLowerCase() : null); + if (dir && dir !== this.currentDirection) { + this.currentDirection = dir; + if (this.moveInterval) clearInterval(this.moveInterval); + const payload = { nickname: this.nickname, direction: dir, roomId: this.roomId }; + stompClient.publish({ destination: "/app/move", body: JSON.stringify(payload) }); + this.moveInterval = setInterval(() => { + stompClient.publish({ destination: "/app/move", body: JSON.stringify(payload) }); + }, 100); + } + } + + handleKeyUp(event) { + if (!this.input.keyboard.enabled) return; + const dirKeys = ['w', 'a', 's', 'd', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; + if (dirKeys.includes(event.key)) { + if (this.moveInterval) clearInterval(this.moveInterval); + this.moveInterval = null; + this.currentDirection = null; + stompClient.publish({ destination: "/app/move", body: JSON.stringify({ nickname: this.nickname, direction: "stop", roomId: this.roomId }) }); + } + } + + shutdown() { + console.log(`MainScene shutdown: Cleaning up...`); + this.input.keyboard.off('keydown', this.handleKeyDown, this); + this.input.keyboard.off('keyup', this.handleKeyUp, this); + if (this.customizationSub) this.customizationSub.unsubscribe(); + if (this.positionsSub) this.positionsSub.unsubscribe(); + if (this.chatSub) this.chatSub.unsubscribe(); + if (this.moveInterval) clearInterval(this.moveInterval); + // if (this.bgm) { + // this.bgm.stop(); + // this.bgm.destroy(); + // this.bgm = null; + // } + } +} + +export default MainScene; \ No newline at end of file diff --git a/nicknamescene.js b/nicknamescene.js new file mode 100644 index 0000000..8c0f8e8 --- /dev/null +++ b/nicknamescene.js @@ -0,0 +1,109 @@ +class NicknameScene extends Phaser.Scene { + constructor() { + super('NicknameScene'); + } + + preload() { + this.load.image('nickname_bg', 'assets/nicknamebg.png'); + const fontLink = document.createElement('link'); + fontLink.rel = 'stylesheet'; + fontLink.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'; + document.head.appendChild(fontLink); + } + + create() { + const login = async (nickname) => { + try { + const response = await fetch(`${SERVER_URL}/api/users/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ nickname }), + }); + + if (!response.ok) { + const message = await response.text(); + console.error(`로그인 실패 (${response.status}):`, message); + throw new Error(`로그인 실패: ${message}`); + } + + const data = await response.json(); + + window.userInfo = { + nickname: nickname, + userId: data.userId, + roomId: data.roomId || null, + character: data.character || null + }; + + console.log("로그인 성공:", nickname); + this.inputElement.remove(); + this.scene.start('CharacterSelectScene', { nickname: nickname }); + + } catch (error) { + console.error("로그인 오류:", error); + alert("로그인 실패: 닉네임이 중복되었습니다."); + } + }; + + const gameWidth = this.sys.game.config.width; + const gameHeight = this.sys.game.config.height; + this.add.image(gameWidth / 2, gameHeight / 2, 'nickname_bg').setDisplaySize(gameWidth, gameHeight); + + this.add.rectangle(gameWidth / 2, gameHeight / 2, 820, 279, 0xF8F2FC).setOrigin(0.5).setDepth(1); + this.add.rectangle(gameWidth / 2, gameHeight / 2 - 120, 381, 72, 0xB593CC).setOrigin(0.5).setDepth(2); + + this.add.text(gameWidth / 2, gameHeight / 2 - 120, '당신의 이름은?', { + fontFamily: 'Pretendard', + fontSize: '32px', + color: '#ffffff', + fontStyle: 'bold', + align: 'center' + }).setOrigin(0.5).setDepth(3); + + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = '이름을 입력하세요'; + this.inputElement = input; + + Object.assign(input.style, { + position: 'fixed', + left: '50%', + top: 'calc(50% + 20px)', + transform: 'translate(-50%, -50%)', + width: '489px', + height: '60px', + fontSize: '20px', + padding: '16px 24px', + border: '4px solid #B593CC', + borderRadius: '12px', + backgroundColor: '#ffffff', + outline: 'none', + zIndex: '10', + fontFamily: 'Pretendard', + }); + + document.body.appendChild(input); + + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + const nickname = input.value.trim(); + if (!nickname) { + alert('이름을 입력하세요.'); + return; + } + login(nickname); + } + }); + } + + shutdown() { + if (this.inputElement) { + this.inputElement.remove(); + } + } +} + +export default NicknameScene; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..57bcebf --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "front", + "version": "1.0.0", + "main": "BridgeScene.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/KuriBus/front.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/KuriBus/front/issues" + }, + "homepage": "https://github.com/KuriBus/front#readme", + "description": "" +} diff --git a/startscene.js b/startscene.js new file mode 100644 index 0000000..b78024f --- /dev/null +++ b/startscene.js @@ -0,0 +1,65 @@ + +class StartScene extends Phaser.Scene { + constructor() { + super('StartScene'); + } + + preload() { + this.load.image('main_bg', 'assets/main.jpg'); + this.load.image('boy', 'assets/boy1.png'); + this.load.image('girl', 'assets/girl1.png'); + this.load.image('logo', 'assets/logo.png'); + this.load.image('start_button', 'assets/start_button.png'); // 시작하기 버튼 + } + + create() { + // 배경 이미지 + this.add.image(800, 450, 'main_bg').setDisplaySize(1600, 900); + + // 로고 + this.add.image(960, 200, 'logo').setOrigin(0.5).setScale(0.4); + + // 캐릭터 그림자 + // this.add.rectangle(352, 840, 246, 49, 0x878787, 0.5) + // .setScale(1, -1).setAlpha(0.5).setDepth(1).setBlur?.(35); + // this.add.rectangle(634, 843, 246, 49, 0x878787, 0.5) + // .setScale(1, -1).setAlpha(0.5).setDepth(1).setBlur?.(35); + + // 캐릭터 이미지 + this.add.image(350, 640, 'girl').setDisplaySize(325, 405); + this.add.image(630, 642, 'boy').setDisplaySize(325, 405); + + // 설정 버튼 + const settingBtn = this.add.rectangle(110, 107, 94, 94, 0xB593CC) + .setOrigin(0.5).setStrokeStyle(4, 0xffffff).setDepth(2); + this.add.text(110, 107, '⚙', { + fontSize: '36px', + color: '#ffffff' + }).setOrigin(0.5).setDepth(3); + + // 고객센터 버튼 + const helpBtn = this.add.rectangle(220, 107, 94, 94, 0xB593CC) + .setOrigin(0.5).setStrokeStyle(4, 0xffffff).setDepth(2); + this.add.text(220, 107, '?', { + fontSize: '36px', + color: '#ffffff' + }).setOrigin(0.5).setDepth(3); + + // 시작 버튼 (버튼은 png 이미지 또는 Phaser 버튼으로 대체 가능) + const startBtn = this.add.rectangle(1186.5, 669, 381, 94, 0xB593CC) + .setOrigin(0.5).setStrokeStyle(2, 0xffffff).setDepth(3); + this.add.text(1186.5, 669, '▶ 시작하기', { + fontSize: '32px', + fontFamily: 'Pretendard', + fontStyle: 'bold', + color: '#ffffff' + }).setOrigin(0.5).setDepth(4); + + startBtn.setInteractive(); + startBtn.on('pointerdown', () => { + this.scene.start('NicknameScene'); + }); + } +} + +export default StartScene; diff --git a/worldmapscene.js b/worldmapscene.js new file mode 100644 index 0000000..98e93a8 --- /dev/null +++ b/worldmapscene.js @@ -0,0 +1,41 @@ +import { socket } from './game.js'; +import { stompClient } from './game.js'; + +class WorldMapScene extends Phaser.Scene { + constructor() { + super('WorldMapScene'); + } + + preload() { + this.load.image('world_bg', 'assets/worldbg.png'); + } + + create() { + this.add.image(800, 450, 'world_bg').setDisplaySize(1600, 900); + + const createRoomButton = (text, x, y, roomId) => { + const btn = this.add.rectangle(x, y, 287, 72, 0xF8F2FC) + .setStrokeStyle(2, 0xB593CC) + .setInteractive() + .setDepth(2); + + this.add.text(x, y, text, { + font: '28px Pretendard', + color: '#B593CC' + }).setOrigin(0.5).setDepth(3); + + btn.on('pointerdown', () => { + window.userInfo.roomId = roomId; + window.userInfo.path = text; + this.scene.start('MainScene'); + }); + }; + + + createRoomButton('교실', 1250, 410, 1); + createRoomButton('문화공간', 550, 280, 3); + createRoomButton('공원', 500, 700, 2); + } +} + +export default WorldMapScene; \ No newline at end of file