diff --git a/app/src/module_game/java/com/chwl/app/game/core/BaseGameActivity.kt b/app/src/module_game/java/com/chwl/app/game/core/BaseGameActivity.kt new file mode 100644 index 0000000..abb28a8 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/BaseGameActivity.kt @@ -0,0 +1,301 @@ +package com.chwl.app.game.core + +import android.app.Activity +import android.view.View +import android.widget.FrameLayout +import androidx.activity.viewModels +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.lifecycleScope +import androidx.viewbinding.ViewBinding +import com.chwl.app.R +import com.chwl.app.base.BaseViewBindingActivity +import com.chwl.app.common.widget.dialog.DialogManager +import com.chwl.app.game.core.engine.GameEngineAbility +import com.chwl.app.game.data.bean.GameInfoUiState +import com.chwl.app.game.ui.game.GameViewModel +import com.chwl.app.game.ui.result.GameResultDialog +import com.chwl.app.ui.pay.ChargeActivity +import com.chwl.core.bean.game.GameResultBean +import com.chwl.core.support.room.RoomContext +import com.chwl.core.support.room.RoomView +import com.chwl.core.support.room.RoomWidget +import com.chwl.core.utils.net.BalanceNotEnoughExeption +import com.chwl.core.utils.net.ServerException +import com.chwl.library.utils.ResUtil +import com.netease.nim.uikit.StatusBarUtil +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +abstract class BaseGameActivity : BaseViewBindingActivity(), RoomView { + + protected val viewModel: GameViewModel by viewModels() + + protected var widgets: HashMap = HashMap() + protected val gameContext get() = viewModel.gameContextLiveData.value + + protected val stateAbility get() = gameContext?.findAbility(GameStateAbility::class.java.simpleName) + protected val gameEngineAbility + get() = gameContext?.findAbility( + GameEngineAbility::class.java.simpleName + ) + + @Deprecated("Dialog管理后续需要优化") + private var gameInfoDialogManager: DialogManager? = null + + @Deprecated("Dialog管理后续需要优化") + protected var loadingDialogManager: DialogManager? = null + + @Deprecated("Dialog管理后续需要优化") + protected var matchFailedDialogManager: DialogManager? = null + + @Deprecated("Dialog管理后续需要优化") + protected var balanceNotEnoughDialogManager: DialogManager? = null + + private var resultDialog: GameResultDialog? = null + override fun init() { + initView() + initEvent() + initObserver() + initWidgets() + } + + protected open fun initView() { + loadingDialogManager = DialogManager(this) + } + + protected open fun initEvent() { + } + + protected open fun initObserver() { + lifecycleScope.launch { + viewModel.closeRoomFlow.collectLatest { + loadingDialogManager?.dismissDialog() + finish() + } + } + viewModel.gameContextLiveData.observe(this) { + onGameContext(it) + } + } + + protected open fun initWidgets() { + } + + protected open fun onGameContext(gameContext: GameContext) { + gameContext.onViewAttach(this) + initGameEngine(gameContext) + stateAbility?.let { + observeGameInfoUiState(it) + + lifecycleScope.launch { + it.matchFailedFlow.collectLatest { + onMatchFailed() + } + } + + lifecycleScope.launch { + it.gameResultFlow.collectLatest { list -> + onGameResult(list) + } + } + } + } + + private fun observeGameInfoUiState(stateAbility: GameStateAbility) { + lifecycleScope.launch { + stateAbility.gameInfoUiStateFlow.collectLatest { + when (it) { + is GameInfoUiState.Loading -> { + if (gameInfoDialogManager == null) { + gameInfoDialogManager = DialogManager(context) + } + gameInfoDialogManager?.showProgressDialog(context) + } + + is GameInfoUiState.Success -> { + matchFailedDialogManager?.dismissDialog() + balanceNotEnoughDialogManager?.dismissDialog() + dismissResultDialog() + gameInfoDialogManager?.dismissDialog() + } + + is GameInfoUiState.Failed -> { + gameInfoDialogManager?.dismissDialog() + if (it.throwable is ServerException && it.throwable.code == BalanceNotEnoughExeption.code) { + onBalanceNotEnough() + } else { + it.throwable.message?.let { msg -> + toast(msg) + } + } + } + + is GameInfoUiState.Empty -> { + gameInfoDialogManager?.dismissDialog() + toast(R.string.game_end_tips) + finish() + } + } + } + } + } + + protected open fun initGameEngine(gameContext: GameContext) { + val gameEngineAbility = gameContext.findAbility(GameEngineAbility::class.java.simpleName) + lifecycleScope.launch { + gameEngineAbility?.gameViewFlow?.collectLatest { + + onGameViewChanged(it) + } + } + } + + protected open fun onGameResult(list: List?) { + if (list == null) { + return + } + dismissResultDialog() + resultDialog = GameResultDialog(list, onClose = { + closeRoom() + }, onRestart = { + restart() + }) + resultDialog?.show(supportFragmentManager, "GAME_RESULT") + } + + private fun onMatchFailed() { + if (matchFailedDialogManager == null) { + matchFailedDialogManager = DialogManager(context) + } + matchFailedDialogManager?.showOkCancelDialog(null, + getString(R.string.game_match_failed), + getString(R.string.game_rematch), + getString(R.string.exit_text), false, false, object : + DialogManager.OkCancelDialogListener { + override fun onOk() { + restart() + } + + override fun onCancel() { + super.onCancel() + closeRoom() + } + }) + } + + protected open fun onBalanceNotEnough() { + if (balanceNotEnoughDialogManager == null) { + balanceNotEnoughDialogManager = DialogManager(this) + } + balanceNotEnoughDialogManager?.showOkCancelDialog( + ResUtil.getString(R.string.widget_dialog_dialoguihelper_04), + ResUtil.getString(R.string.treasure_to_charge) + ) { + ChargeActivity.start(context) + } + } + + private fun closeRoom() { + loadingDialogManager?.showProgressDialog(this@BaseGameActivity) + viewModel.closeRoom() + } + + private fun restart() { + val intent = viewModel.getGameIntent() + if (intent == null) { + toast(R.string.data_error) + return + } + stateAbility?.restart(intent) + } + + protected open fun onGameViewChanged(view: View?) { + if (view == null) { + getGameViewGroup().removeAllViews() + } else { + getGameViewGroup().addView( + view, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + ) + } + } + + abstract fun getGameViewGroup(): FrameLayout + + /** + * 注册组件 + */ + protected open fun registerWidget(name: String, widget: RoomWidget) { + widgets[name] = widget + widget.onStart(this) + } + + /** + * 取消注册组件 + */ + protected open fun unregisterWidgets() { + widgets.values.forEach { + it.onStop() + } + widgets.clear() + } + + override fun needSteepStateBar(): Boolean { + return true + } + + override fun setStatusBar() { + super.setStatusBar() + StatusBarUtil.transparencyBar(this) + } + + override fun getLifecycleOwner(): LifecycleOwner { + return this + } + + override fun getActivity(): Activity? { + return this + } + + override fun getRoomContext(): RoomContext? { + return viewModel.gameContextLiveData.value + } + + override fun getViewFragmentManager(): FragmentManager { + return supportFragmentManager + } + + override fun getRoomContextLiveData(): LiveData { + return viewModel.gameContextLiveData + } + + override fun findWidget(name: String): RoomWidget? { + return widgets[name] + } + + override fun onDestroy() { + super.onDestroy() + balanceNotEnoughDialogManager?.dismissDialog() + balanceNotEnoughDialogManager = null + gameInfoDialogManager?.dismissDialog() + gameInfoDialogManager = null + loadingDialogManager?.dismissDialog() + loadingDialogManager = null + dismissResultDialog() + resultDialog = null + matchFailedDialogManager?.dismissDialog() + matchFailedDialogManager = null + } + + private fun dismissResultDialog() { + try { + resultDialog?.dismissAllowingStateLoss() + } catch (e: Exception) { + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/GameContext.kt b/app/src/module_game/java/com/chwl/app/game/core/GameContext.kt new file mode 100644 index 0000000..f63b6da --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/GameContext.kt @@ -0,0 +1,26 @@ +package com.chwl.app.game.core + +import com.chwl.app.game.core.engine.GameEngineAbility +import com.chwl.core.bean.game.GameRoomInfo +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext + +class GameContext(roomId: Long) : RoomContext(roomId) { + + override fun loadAbility(list: MutableMap) { + super.loadAbility(list) + list[GameStateAbility::class.java.simpleName] = GameStateAbility() + list[GameEngineAbility::class.java.simpleName] = GameEngineAbility() + list[GameIMEngineAbility::class.java.simpleName] = GameIMEngineAbility() + list[GameQueueAbility::class.java.simpleName] = GameQueueAbility() + list[GameMessageAbility::class.java.simpleName] = GameMessageAbility() + } + + override fun performStart() { + super.performStart() + } + + fun restart(roomInfo: GameRoomInfo) { + val stateAbility = findAbility(GameStateAbility::class.java.simpleName) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/GameIMEngineAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/GameIMEngineAbility.kt new file mode 100644 index 0000000..05d4db1 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/GameIMEngineAbility.kt @@ -0,0 +1,137 @@ +package com.chwl.app.game.core + +import com.chwl.app.public_chat.core.ChatRoomClient +import com.chwl.app.public_chat.core.ChatRoomClientManager +import com.chwl.core.manager.IMNetEaseManager +import com.chwl.core.support.listener.ListenerOwner +import com.chwl.core.support.listener.ListenerStore +import com.chwl.core.support.listener.SafeListenerOwner +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext +import com.netease.nimlib.sdk.StatusCode +import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder +import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage +import io.reactivex.Single +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine + +class GameIMEngineAbility(private val listenerOwner: ListenerOwner = SafeListenerOwner()) : + RoomAbility(), ListenerStore by listenerOwner { + + val stateFlow = MutableStateFlow(StatusCode.INVALID) + + // 恢复连接(中断后恢复) + val reconnectedFlow = MutableSharedFlow() + + private var chatRoomClient: ChatRoomClient? = null + + private var netBrokenTag = false + + private val stateAbility: GameStateAbility? + get() = roomContext?.findAbility( + GameStateAbility::class.java.simpleName + ) + + override fun onStart(context: RoomContext) { + super.onStart(context) + safeLaunch { + stateAbility?.let { + combine( + *arrayOf( + it.imIdFlow, + it.gameStateFlow + ), transform = { + true + }) + .collectLatest { + checkRunning() + } + } + } + } + + private fun checkRunning() { + val stateAbility = this.stateAbility + val gameState = stateAbility?.gameStateFlow?.value + val sessionId = stateAbility?.imIdFlow?.value?.toString() + logD("checkRunning() gameState:$gameState sessionId:$sessionId chatRoomClient?.sessionId = ${chatRoomClient?.sessionId} GameIMEngineAbility ") + if (sessionId != null && (gameState == GameStateAbility.STATE_MATCHING || gameState == GameStateAbility.STATE_MATCH_SUCCESS)) { + if (chatRoomClient?.sessionId != sessionId) { + logD("checkRunning() 1 GameIMEngineAbility ") + exitRoom() + initClient(sessionId) + } + } else { + logD("checkRunning() 2 GameIMEngineAbility ") + exitRoom() + } + } + + private fun exitRoom() { + logD("exitRoom() id:${chatRoomClient?.sessionId}") + IMNetEaseManager.gameChatRoomId = null + chatRoomClient?.onCleared() + chatRoomClient = null + } + + private fun initClient(sessionId: String) { + logD("GameIMEngineAbility initClient() sessionId:$sessionId") + netBrokenTag = false + chatRoomClient = ChatRoomClientManager.getClient(sessionId) + chatRoomClient?.let { + safeLaunch { + it.stateFlow.collect { + if (it == StatusCode.NET_BROKEN) { + netBrokenTag = true + } + if (it == StatusCode.LOGINED) { + if (netBrokenTag) { + reconnectedFlow.emit(true) + } + netBrokenTag = false + } + this@GameIMEngineAbility.stateFlow.value = it + } + } + addDisposable(it.messageObservable.subscribe { list -> + listenerOwner.postEvent { it.onReceiveMessage(list) } + }) + enterChatRoom(it) + } + } + + private fun enterChatRoom(client: ChatRoomClient) { + IMNetEaseManager.gameChatRoomId = client.sessionId + addDisposable(client.enterChatRoom().subscribe({ + }, { + })) + } + + fun sendMessage(message: ChatRoomMessage) { + chatRoomClient?.let { + addDisposable(it.sendMessage(message).subscribe({}, {})) + } + } + + fun buildTextMessage(text: String): ChatRoomMessage? { + val client = chatRoomClient ?: return null + return ChatRoomMessageBuilder.createChatRoomTextMessage(client.sessionId, text) + } + + fun sendMessageRx(message: ChatRoomMessage): Single { + val client = chatRoomClient ?: return Single.error(IllegalStateException("client is NULL")) + return client.sendMessage(message) + } + + override fun onStop(context: RoomContext) { + super.onStop(context) + listenerOwner.removeAllListener() + exitRoom() + } + + interface Listener { + fun onReceiveMessage(messages: List) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/GameMessageAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/GameMessageAbility.kt new file mode 100644 index 0000000..c43fbc2 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/GameMessageAbility.kt @@ -0,0 +1,121 @@ +package com.chwl.app.game.core + +import com.chwl.app.R +import com.chwl.core.support.listener.ListenerOwner +import com.chwl.core.support.listener.ListenerStore +import com.chwl.core.support.listener.SafeListenerOwner +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext +import com.chwl.library.utils.ResUtil +import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder +import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage +import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum +import io.reactivex.Single +import kotlinx.coroutines.flow.collectLatest +import java.lang.IllegalStateException + +class GameMessageAbility(private val listenerOwner: ListenerOwner = SafeListenerOwner()) : + RoomAbility(), ListenerStore by listenerOwner, + GameIMEngineAbility.Listener { + + private val dataList = ArrayList() + + private val imEngineAbility + get() = + roomContext?.findAbility(GameIMEngineAbility::class.java.simpleName) + + override fun onStart(context: RoomContext) { + super.onStart(context) + val imEngineAbility = + context.findAbility(GameIMEngineAbility::class.java.simpleName) + imEngineAbility?.addListener(this) + val stateAbility = + context.findAbility(GameStateAbility::class.java.simpleName) + stateAbility?.let { + safeLaunch { + it.imIdFlow.collectLatest { + resetMessageList() + } + } + + safeLaunch { + it.gameStateFlow.collectLatest { + if (it == GameStateAbility.STATE_GAME_END) { + resetMessageList() + } + } + } + } + } + + private fun resetMessageList() { + val stateAbility = + roomContext?.findAbility(GameStateAbility::class.java.simpleName) + dataList.clear() + val sessionId = stateAbility?.imIdFlow?.value + if (sessionId != null) { + dataList.add(getSystemNotifyMessage(sessionId.toString())) + } + listenerOwner.postEvent { + it.onReplaceMessageList(dataList) + } + } + + fun getMessageList(): List { + return dataList + } + + fun addMessage(message: Any) { + dataList.add(message) + listenerOwner.postEvent { + it.onAddMessage(message) + } + } + + fun sendTextMessage(text: String): Single { + imEngineAbility?.let { + val textMessage = it.buildTextMessage(text) + if (textMessage == null) { + return Single.error(IllegalStateException("message is NULL")) + } else { + return it.sendMessageRx(textMessage).map { + addMessage(textMessage) + } + } + } + return Single.error(IllegalStateException("engine is NULL")) + } + + private fun getSystemNotifyMessage(sessionId: String): ChatRoomMessage { + val message = ChatRoomMessageBuilder.createTipMessage( + sessionId + ) + message.content = ResUtil.getString( + R.string.yizhuan_xchat_android_constants_xchatconstants_01 + ) + return message + } + + override fun onStop(context: RoomContext) { + super.onStop(context) + listenerOwner.removeAllListener() + } + + override fun onReceiveMessage(messages: List) { + messages.forEach { + onReceiveMessage(it) + } + } + + private fun onReceiveMessage(message: ChatRoomMessage) { + if (message.msgType == MsgTypeEnum.text) { + addMessage(message) + } + } + + interface Listener { + fun onAddMessage(message: Any) + + fun onReplaceMessageList(list: List) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/GameQueueAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/GameQueueAbility.kt new file mode 100644 index 0000000..1f0f43b --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/GameQueueAbility.kt @@ -0,0 +1,25 @@ +package com.chwl.app.game.core + +import com.chwl.core.bean.room.RoomMicBean +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext +import kotlinx.coroutines.flow.MutableStateFlow + +class GameQueueAbility : RoomAbility() { + + val queueFlow = MutableStateFlow?>(null) + + override fun onStart(context: RoomContext) { + super.onStart(context) + } + + suspend fun updateQueue(data: List?) { + queueFlow.value = data + } + + fun findQueueItem(uid: Long): RoomMicBean? { + return queueFlow.value?.firstOrNull { + it.micUser?.uid == uid + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/GameStateAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/GameStateAbility.kt new file mode 100644 index 0000000..512f95d --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/GameStateAbility.kt @@ -0,0 +1,237 @@ +package com.chwl.app.game.core + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.alibaba.fastjson2.JSON +import com.chwl.app.game.data.GameModel2 +import com.chwl.app.game.data.bean.GameInfoUiState +import com.chwl.app.game.ui.game.GameIntent +import com.chwl.core.bean.game.GameResultBean +import com.chwl.core.bean.game.GameRoomInfo +import com.chwl.core.im.custom.bean.CustomAttachment +import com.chwl.core.im.custom.bean.GameForcedEndAttachment +import com.chwl.core.im.custom.bean.GameQueueChangedAttachment +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext +import com.chwl.library.common.util.doLog +import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage +import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest + +class GameStateAbility : RoomAbility(), GameIMEngineAbility.Listener { + + companion object { + // 匹配中 + const val STATE_MATCHING = 0 + + // 匹配成功 + const val STATE_MATCH_SUCCESS = 1 + + // 游戏结束 + const val STATE_GAME_END = 2 + + // 匹配失败 + const val STATE_MATCH_FAILED = 3 + + // 错误状态 + const val STATE_ERROR = -1 + } + + /** + * 游戏状态: + * 匹配状态(服务端定义):(0:匹配中、1:匹配成功、2:游戏结束、3:匹配失败) + */ + val gameStateFlow: MutableStateFlow = MutableStateFlow(null) + + val gameIconFlow = MutableStateFlow(null) + + val gameIdFlow = MutableStateFlow(null) + + val roomIdFlow = MutableStateFlow(null) + + val imIdFlow = MutableStateFlow(null) + + val matchRoundIdFlow = MutableStateFlow(null) + + val scoresFlow = MutableStateFlow?>(null) + + val gameConfigFlow = MutableStateFlow(null) + + @Deprecated("里面的属性有可能不是最新的,建议通过具体属性flow获取数据") + val gameInfoFlow = MutableStateFlow(null) + + val gameInfoUiStateFlow = MutableStateFlow(GameInfoUiState.Loading) + + val matchFailedFlow = MutableSharedFlow() + + val gameResultFlow = MutableSharedFlow?>() + + private var viewBackground = false + + private val queueAbility get() = roomContext?.findAbility(GameQueueAbility::class.java.simpleName) + + override fun onStart(context: RoomContext) { + super.onStart(context) + requestRoomInfo(true) + val imEngineAbility = + context.findAbility(GameIMEngineAbility::class.java.simpleName) + imEngineAbility?.addListener(this) + safeLaunch { + imEngineAbility?.reconnectedFlow?.collectLatest { + requestSyncState(true) + } + } + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + super.onStateChanged(source, event) + when (event) { + Lifecycle.Event.ON_RESUME -> { + if (viewBackground) { + if (gameStateFlow.value == STATE_MATCHING || gameStateFlow.value == STATE_MATCH_SUCCESS) { + requestSyncState(false) + } + } + viewBackground = false + } + + Lifecycle.Event.ON_STOP -> { + //todo do 暂时注释这个 , 游戏目前没有最小化 +// viewBackground = true + } + + else -> { + } + } + } + + // 请求同步服务端的最新状态 + private fun requestSyncState(needLoading: Boolean) { + logD("requestSyncState") + requestRoomInfo(needLoading) + } + + private fun requestRoomInfo(needLoading: Boolean) { + logD("requestRoomInfo needLoading:$needLoading") + if (needLoading) { + gameInfoUiStateFlow.value = GameInfoUiState.Loading + } + safeLaunch(onError = { + logD("requestRoomInfo Failed:$it") + gameInfoUiStateFlow.value = GameInfoUiState.Failed(it, gameInfoFlow.value) + }) { + val info = GameModel2.getGameRoomInfo() //chatRoom/getByType + if (info != null) { + gameInfoUiStateFlow.value = GameInfoUiState.Success + info.data?.matchStatus = 0 + logD("飞行棋 请求 游戏信息成功 requestRoomInfo Success =${JSON.toJSONString(info)}") + syncRoomInfo(info) + } else { + logD("requestRoomInfo Empty") + gameInfoUiStateFlow.value = GameInfoUiState.Empty(gameInfoFlow.value) + syncGameState(STATE_ERROR) + } + } + } + + fun restart(intent: GameIntent) { + gameInfoUiStateFlow.value = GameInfoUiState.Loading + safeLaunch(onError = { + gameInfoUiStateFlow.value = GameInfoUiState.Failed(it, gameInfoFlow.value) + }) { + GameModel2.startGame(intent.gameId.toString(), intent.gameMode) + requestRoomInfo(true) + } + } + + private suspend fun syncRoomInfo(info: GameRoomInfo) { + "飞行棋 同步房间信息 syncRoomInfo ${info}".doLog() + roomContext?.roomId = info.roomId ?: 0 + + gameInfoFlow.value = info + roomIdFlow.value = info.roomId + imIdFlow.value = info.roomId + gameIdFlow.value = info.data?.mgId?.toLongOrNull() + gameIconFlow.value = info.data?.gameRoomIcon + info.data?.scores?.let { + scoresFlow.value = it + } + gameConfigFlow.value = info.data?.configJson + matchRoundIdFlow.value = info.data?.matchRoundId + + syncQueue(info) + // 最后变更状态 + syncGameState(info.data?.matchStatus) + } + + fun syncGameState(matchStatus: Int?) { + gameStateFlow.value = matchStatus + } + + private suspend fun syncQueue(info: GameRoomInfo) { + "飞行棋 同步麦位信息 syncQueue ${info}".doLog() + gameIdFlow?.value = info.data?.mgId?.toLongOrNull() + info.data?.scores?.let { + scoresFlow.value = it + } + queueAbility?.updateQueue(info.roomMics) + matchRoundIdFlow.value = info.data?.matchRoundId + syncGameState(info.data?.matchStatus) + } + + override fun onReceiveMessage(messages: List) { + messages.forEach { + if (it.msgType == MsgTypeEnum.custom) { + val attachment: CustomAttachment = (it.attachment as? CustomAttachment) ?: return + onReceiveCustomMessage(it, attachment) + } + } + } + + private fun onReceiveCustomMessage(message: ChatRoomMessage, attachment: CustomAttachment) { + when (attachment.first) { + CustomAttachment.CUSTOM_MSG_MINI_GAME -> { + when (attachment.second) { + // 麦位变更 + CustomAttachment.CUSTOM_MSG_MINI_GAME_QUEUE_CHANGED -> { + logD("飞行棋 onReceiveMessage 麦位变更") + val gameInfo = (attachment as? GameQueueChangedAttachment)?.gameInfo ?: return + if (gameInfo.roomId != roomContext?.roomId) { + logD("onReceiveMessage 房间变更") + safeLaunch { + syncRoomInfo(gameInfo) + } + } else { + safeLaunch { + syncQueue(gameInfo) + } + } + } + + // 匹配失败 + CustomAttachment.CUSTOM_MSG_MINI_GAME_MATCH_FAILED -> { + logD("onReceiveMessage 匹配失败") + safeLaunch { + syncGameState(STATE_MATCH_FAILED) + matchFailedFlow.emit(true) + } + } + + // 提前结束 + CustomAttachment.CUSTOM_MSG_MINI_GAME_FORCED_END -> { + logD("onReceiveMessage 提前结束") + val data = + (attachment as? GameForcedEndAttachment)?.msgData ?: return + val results = data.results + safeLaunch { + syncGameState(data.matchStatus) + gameResultFlow.emit(results) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/engine/GameEngineAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/engine/GameEngineAbility.kt new file mode 100644 index 0000000..e66ecc6 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/engine/GameEngineAbility.kt @@ -0,0 +1,226 @@ +package com.chwl.app.game.core.engine + +import com.chwl.app.game.core.GameQueueAbility +import com.chwl.app.game.core.GameStateAbility +import com.chwl.core.auth.AuthModel +import com.chwl.core.bean.game.GameResultBean +import com.chwl.core.bean.game.SudGameConfigBean +import com.chwl.core.sud.model.GameViewInfoModel +import com.chwl.core.sud.state.SudMGPAPPState +import com.chwl.core.sud.state.SudMGPMGState +import com.chwl.core.support.room.RoomContext +import com.chwl.library.utils.json.JsonUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import org.json.JSONObject +import tech.sud.mgp.core.ISudFSMStateHandle + +class GameEngineAbility : SudEngineAbility() { + private val stateAbility get() = roomContext?.findAbility(GameStateAbility::class.java.simpleName) + private var gameConfig: SudGameConfigBean? = null + val gameViewRectFlow = MutableStateFlow(null) + + override fun onStart(context: RoomContext) { + super.onStart(context) + safeLaunch { + stateAbility?.gameConfigFlow?.collectLatest { + parseGameConfig(it) + } + } + safeLaunch { + stateAbility?.let { + combine( + *arrayOf( + gameViewRectFlow, + it.gameIdFlow, + it.roomIdFlow, + it.gameStateFlow + ), transform = { + true + }) + .collectLatest { + checkRunning() + } + } + } + } + + private fun checkRunning() { + logD("checkRunning() roomIdFlow = ${stateAbility?.roomIdFlow?.value} GameEngineAbility ") + if (gameViewRectFlow.value == null) { + logD("checkRunning() gameViewRect:null GameEngineAbility ") + return + } + val stateAbility = this.stateAbility + val gameState = stateAbility?.gameStateFlow?.value + logD("checkRunning() gameState:$gameState GameEngineAbility ") + if (gameState == GameStateAbility.STATE_MATCH_SUCCESS) { + logD("checkRunning() gameState:$gameState roomView =${roomView?.getActivity()} roomIdFlow=${stateAbility.roomIdFlow.value} gameIdFlow=${stateAbility.gameIdFlow.value} GameEngineAbility -> loadGame()") + val activity = roomView?.getActivity() ?: return + val roomId = stateAbility.roomIdFlow.value ?: return + val gameId = stateAbility.gameIdFlow.value ?: return + loadGame(activity, roomId.toString(), gameId) + } else { + logD("checkRunning() destroyGame GameEngineAbility ") + exitGame() + destroyGame() + } + } + + private fun parseGameConfig(json: String?) { + if (json != null) { + gameConfig = JsonUtils.fromJson(json, SudGameConfigBean::class.java) + } else { + gameConfig = null + } + } + + private fun autoPlayGame() { + if (!isRunning) { + return + } + val stateAbility = stateAbility ?: return + logD("autoPlayGame state:${stateAbility.gameStateFlow.value}") + if (stateAbility.gameStateFlow.value != GameStateAbility.STATE_MATCH_SUCCESS) { + return + } + val queueAbility = + roomContext?.findAbility(GameQueueAbility::class.java.simpleName) + ?: return + val queueSize = queueAbility.queueFlow.value?.size ?: 0 + if (queueSize == 0) { + return + } + logD("autoPlayGame queueSize:$queueSize size:${sudFSMMG.sudFSMMGCache.playerReadySet.size}") + if (sudFSMMG.sudFSMMGCache.playerReadySet.size >= queueSize) { + val roundId = stateAbility.matchRoundIdFlow.value + val jsonObject = JSONObject() + jsonObject.put("value0", roundId) + val extras = jsonObject.toString() + logD("autoPlayGame roundId:${roundId}") + sudFSTAPP.notifyAPPCommonSelfPlaying(true, extras, "value0") + } + } + + private fun autoJoinGame() { + if (!isRunning) { + return + } + logD("autoJoinGame") + sudFSTAPP.notifyAPPCommonSelfIn(true, -1, true, 1) + sudFSTAPP.notifyAPPCommonSelfReady(true) + + + + val queueAbility = roomContext?.findAbility(GameQueueAbility::class.java.simpleName) + val firstOrNull = queueAbility?.queueFlow?.value?.firstOrNull { it?.micUser?.uid != AuthModel.get().currentUid } + firstOrNull?.micUser?.let { user-> + if (user.aiLevel != null) { + sudFSTAPP.notifyAPPCommonGameAddAIPlayers(listOf(SudMGPAPPState.AIPlayers().apply { + userId = user.uid?.toString() + name = user.nick + avatar = user.avatar + level = user.aiLevel?:SudMGPAPPState.AIPlayers.LEVEL_MODERATE +// gender + }),1); + } + } + } + + fun setGameViewRect(rect: GameViewInfoModel.GameViewRectModel) { + this.gameViewRectFlow.value = rect + } + + override fun getGameViewRect(): GameViewInfoModel.GameViewRectModel { + return this.gameViewRectFlow.value ?: super.getGameViewRect() + } + + override fun onGameStarted() { + super.onGameStarted() + logD("onGameStarted") + autoJoinGame() + autoPlayGame() + updateGameSettingSelectConfig() + } + + private fun updateGameSettingSelectConfig() { + val configStr = gameConfig?.getGameSettingSelectConfigStr() + if (!configStr.isNullOrEmpty()) { + logD("updateGameSettingSelectConfig() configStr:$configStr") + sudFSTAPP.notifyStateChange(SudMGPAPPState.APP_COMMON_GAME_SETTING_SELECT_INFO, configStr) + } + } + + override fun onPlayerMGCommonPlayerReady( + handle: ISudFSMStateHandle?, + userId: String?, + model: SudMGPMGState.MGCommonPlayerReady? + ) { + super.onPlayerMGCommonPlayerReady(handle, userId, model) + logD("onPlayerMGCommonPlayerReady userId:$userId model:${model?.isReady}") + if (model?.isReady != false) { + autoPlayGame() + } + } + + override fun onGameMGCommonGameSettle( + handle: ISudFSMStateHandle, + model: SudMGPMGState.MGCommonGameSettle? + ) { + super.onGameMGCommonGameSettle(handle, model) + val list = ArrayList() + val queueAbility = roomContext?.findAbility(GameQueueAbility::class.java.simpleName) + val stateAbility = roomContext?.findAbility(GameStateAbility::class.java.simpleName) + val scoresList = stateAbility?.scoresFlow?.value + model?.results?.sortedBy { it.rank }?.forEachIndexed { index, it -> + val uid = it.uid?.toLong() ?: -1 + val queueItem = queueAbility?.findQueueItem(uid) + val scores = scoresList?.getOrNull(index) + val item = GameResultBean().apply { + this.uid = it.uid.toLongOrNull() + this.rank = it.rank + this.nick = queueItem?.micUser?.nick + this.avatar = queueItem?.micUser?.avatar + this.winNum = scores + // 目前1v1:第一名成功(后面需要调整这个逻辑) + this.isWin = index == 0 + this.isEscaped = it.isEscaped == 1 + } + list.add(item) + } + safeLaunch { + stateAbility?.gameResultFlow?.emit(list) + stateAbility?.syncGameState(GameStateAbility.STATE_GAME_END) + } + } + + override fun onGetGameCfg(handle: ISudFSMStateHandle, p1: String?) { + val configStr = gameConfig?.getGameConfigStr() + logD("onGetGameCfg() configStr:$configStr") + if (configStr.isNullOrEmpty()) { + super.onGetGameCfg(handle, p1) + } else { + handle.success(configStr) + } + } + + override fun onPlayerStateChange( + handle: ISudFSMStateHandle?, + userId: String?, + state: String?, + dataJson: String? + ): Boolean { + logD("onPlayerStateChange userId:$userId state:$state dataJson:$dataJson") + return super.onPlayerStateChange(handle, userId, state, dataJson) + } + + override fun onGameStateChange( + handle: ISudFSMStateHandle?, + state: String?, + dataJson: String? + ): Boolean { + logD("onGameStateChange state:$state dataJson:${dataJson}") + return super.onGameStateChange(handle, state, dataJson) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/core/engine/SudEngineAbility.kt b/app/src/module_game/java/com/chwl/app/game/core/engine/SudEngineAbility.kt new file mode 100644 index 0000000..a7cc0fa --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/core/engine/SudEngineAbility.kt @@ -0,0 +1,298 @@ +package com.chwl.app.game.core.engine + +import android.annotation.SuppressLint +import android.app.Activity +import android.view.View +import android.view.ViewTreeObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.chwl.app.R +import com.chwl.app.avroom.game.AppConfig +import com.chwl.core.auth.AuthModel +import com.chwl.core.room.game.GameModel +import com.chwl.core.sud.decorator.SudFSMMGDecorator +import com.chwl.core.sud.decorator.SudFSMMGListener +import com.chwl.core.sud.decorator.SudFSTAPPDecorator +import com.chwl.core.sud.model.GameConfigModel +import com.chwl.core.sud.model.GameViewInfoModel +import com.chwl.core.sud.state.MGStateResponse +import com.chwl.core.sud.state.SudMGPMGState +import com.chwl.core.support.room.RoomAbility +import com.chwl.core.support.room.RoomContext +import com.chwl.library.language.LanguageHelper +import com.chwl.library.utils.SingleToastUtil +import com.chwl.library.utils.json.JsonUtils +import com.example.lib_utils.log.ILog +import kotlinx.coroutines.flow.MutableStateFlow +import tech.sud.mgp.core.ISudFSMStateHandle +import tech.sud.mgp.core.ISudListenerInitSDK +import tech.sud.mgp.core.SudMGP + +@SuppressLint("StaticFieldLeak") +open class SudEngineAbility : RoomAbility(), SudFSMMGListener, ILog { + + val gameViewFlow = MutableStateFlow(null) + protected var isRunning = false + protected val sudFSMMG = SudFSMMGDecorator().apply { + setSudFSMMGListener(this@SudEngineAbility) + } + protected val sudFSTAPP = SudFSTAPPDecorator() + private var roomId: String = "" + private var gameId: Long = 0 + + fun loadGame(activity: Activity, roomId: String, gameId: Long) { + logD("loadGame() roomId:$roomId gameId:$gameId") + if (isRunning && roomId == getRoomId() && gameId == getGameId()) { + logD("loadGame() 重复") + return + } + if (activity.isFinishing || activity.isDestroyed) { + logD("loadGame() activity 状态异常") + return + } + exitGame() + destroyGame() + this.isRunning = true + this.roomId = roomId + this.gameId = gameId + getGameCode({ + initSDK(activity, gameId, it) + }, { + toast(it.message) + }) + } + + // 该用户是否在游戏中 + fun playerIsPlaying(userId: Long): Boolean { + return sudFSMMG.playerIsPlaying(userId.toString()) + } + + // 返回该玩家是否已加入了游戏 + fun playerIsIn(userId: Long): Boolean { + return sudFSMMG.playerIsIn(userId.toString()) + } + + // 自己是否已加入了游戏 + fun isSelfInGame(): Boolean { + return playerIsIn(getUserId()) + } + + fun exitGame() { + if (!isRunning) { + return + } + val userId = getUserId() + if (playerIsPlaying(userId)) { + logD("exitGame() 1") + // 用户正在游戏中,先退出本局游戏,再退出游戏 + sudFSTAPP.notifyAPPCommonSelfPlaying(false, null, null) + sudFSTAPP.notifyAPPCommonSelfReady(false) + sudFSTAPP.notifyAPPCommonSelfIn(false, -1, true, 1) + } else if (sudFSMMG.playerIsReady(userId.toString())) { + logD("exitGame() 2") + // 用户已加入并且已经准备,先取消准备,再退出游戏 + sudFSTAPP.notifyAPPCommonSelfReady(false) + sudFSTAPP.notifyAPPCommonSelfIn(false, -1, true, 1) + } else if (isSelfInGame()) { + logD("exitGame() 3") + // 用户已加入游戏 退出游戏 + sudFSTAPP.notifyAPPCommonSelfIn(false, -1, true, 1) + } + } + + fun destroyGame() { + if (!isRunning) { + return + } + logD("destroyGame()") + isRunning = false + sudFSTAPP.destroyMG() + sudFSMMG.destroyMG() + gameViewFlow.value = null + roomId = "" + gameId = 0 + } + + private fun initSDK(activity: Activity, gameId: Long, code: String) { + logD("initSDK isTestEnv() = ${isTestEnv()}") + SudMGP.initSDK( + activity, + getAppId(), + getAppKey(), + isTestEnv(), + object : ISudListenerInitSDK { + override fun onSuccess() { + loadGameSDK(activity, gameId, code) + } + + override fun onFailure(code: Int, errInfo: String) { + logD("initSDK onFailure code:$code errInfo:$errInfo") + val msg = + activity.getString(R.string.game_failed_tips).format("$code-${errInfo}") + toast(msg) + } + }) + } + + private fun loadGameSDK(activity: Activity, gameId: Long, code: String) { + if (!isRunning) { + return + } + logD("loadGameSDK") + sudFSTAPP.destroyMG() + sudFSMMG.destroyMG() + sudFSMMG.setSudFSMMGListener(this) + val iSudFSTAPP = + SudMGP.loadMG(activity, getUserId().toString(), getRoomId(), code, gameId, getGameLanguage(), sudFSMMG) + sudFSTAPP.setISudFSTAPP(iSudFSTAPP) + gameViewFlow.value = iSudFSTAPP.gameView + + } + + private fun getGameCode(onSuccess: (String) -> Unit, onFailed: (Throwable) -> Unit) { + addDisposable( + GameModel.getGameCode() + .subscribe({ + onSuccess.invoke(it.code) + }, { + onFailed.invoke(it) + }) + ) + } + + private fun toast(message: String?) { + if (!message.isNullOrEmpty()) { + SingleToastUtil.showToast(message) + } + } + + private fun notifyGameViewInfo(handle: ISudFSMStateHandle, width: Int, height: Int) { + val response = GameViewInfoModel() + response.ret_code = 0 + response.view_size.width = width + response.view_size.height = height + response.view_game_rect = getGameViewRect() + handle.success(JsonUtils.toJson(response)) + } + + override fun onStop(context: RoomContext) { + super.onStop(context) + logD("onStop") + destroyGame() + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + super.onStateChanged(source, event) + logD("onStateChanged event:$event") + when (event) { + Lifecycle.Event.ON_START -> { + sudFSTAPP.startMG() + } + + Lifecycle.Event.ON_STOP -> { + sudFSTAPP.stopMG() + } + + Lifecycle.Event.ON_RESUME -> { + sudFSTAPP.playMG() + } + + Lifecycle.Event.ON_PAUSE -> { + sudFSTAPP.pauseMG() + } + + Lifecycle.Event.ON_DESTROY -> { + destroyGame() + } + + else -> { + + } + } + } + + override fun onGameStarted() { + logD("onGameStarted") + } + + override fun onGameDestroyed() { + logD("onGameDestroyed") + } + + override fun onExpireCode(handle: ISudFSMStateHandle, p1: String?) { + logD("onExpireCode") + getGameCode({ + handle.success(MGStateResponse.success().toJson()) + sudFSTAPP.updateCode(it, null) + }, { + logE(it) + }) + } + + override fun onGetGameViewInfo(handle: ISudFSMStateHandle, p1: String?) { + logD("onGetGameViewInfo") + val gameLayout = gameViewFlow.value + if (gameLayout == null) { + handle.failure("gameLayout is NULL") + return + } + val gameViewWidth = gameLayout.measuredWidth + val gameViewHeight = gameLayout.measuredHeight + if (gameViewWidth > 0 && gameViewHeight > 0) { + notifyGameViewInfo(handle, gameViewWidth, gameViewHeight) + return + } + gameLayout.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + gameLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) + notifyGameViewInfo(handle, gameLayout.measuredWidth, gameLayout.measuredHeight) + } + }) + } + + override fun onGetGameCfg(handle: ISudFSMStateHandle, p1: String?) { + logD("onGetGameCfg") + handle.success(JsonUtils.toJson(GameConfigModel())) + } + + override fun onGameMGCommonSelfClickJoinBtn( + handle: ISudFSMStateHandle?, + model: SudMGPMGState.MGCommonSelfClickJoinBtn? + ) { + logD("onGameMGCommonSelfClickJoinBtn") + sudFSTAPP.notifyAPPCommonSelfIn(true, -1, true, 1) + } + + private fun getRoomId() = roomId + + private fun getGameId() = gameId + + protected open fun getUserId(): Long = AuthModel.get().currentUid + + protected open fun getAppId() = AppConfig.APP_ID + + protected open fun getAppKey() = AppConfig.APP_KEY + + protected open fun isTestEnv() = AppConfig.isTestEnv + + protected open fun getGameViewRect(): GameViewInfoModel.GameViewRectModel { + return GameViewInfoModel.GameViewRectModel() + } + + protected open fun getGameLanguage(): String { + return when (LanguageHelper.getCurrentLanguageType()) { + LanguageHelper.ZH -> { + "zh-TW" + } + + LanguageHelper.AR -> { + "ar-SA" + } + + else -> { + "en-US" + } + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/data/GameModel2.kt b/app/src/module_game/java/com/chwl/app/game/data/GameModel2.kt new file mode 100644 index 0000000..c0b5457 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/data/GameModel2.kt @@ -0,0 +1,99 @@ +package com.chwl.app.game.data + +import com.chwl.core.base.BaseModel +import com.chwl.core.bean.game.GameConfigBean +import com.chwl.core.bean.game.GameRoomInfo +import com.chwl.core.bean.response.ServiceResult +import com.chwl.core.bean.room.BaseRoomInfo +import com.chwl.core.pay.PayModel +import com.chwl.core.utils.net.RxHelper +import com.chwl.core.utils.net.launchRequest +import com.chwl.library.net.rxnet.RxNet +import io.reactivex.Single +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +object GameModel2 : BaseModel() { + + private val api = RxNet.create(Api::class.java) + + suspend fun getHomeGameConfig(): GameConfigBean? = + launchRequest { + api.getHomeGameConfig() + } + + fun getResumeGameRoomInfo(): Single { + return api.getResumeGameRoom() + .compose(RxHelper.handleCommon(RxHelper.NullHandle { return@NullHandle GameRoomInfo() })) + .compose(RxHelper.handleSchAndExce()) + } + + suspend fun getGameRoomInfo(): GameRoomInfo? = + launchRequest { + api.getChatRoomInfo(BaseRoomInfo.ROOM_TYPE_GAME) + } + + suspend fun startGame(gameId: String, gameMode: Int): String? = + launchRequest { + val result = api.startGame(gameId, gameMode) + PayModel.get().refreshWalletInfo(true) + result + } + + suspend fun closeGame(roomId: Long): String? = + launchRequest { + api.closeGame(roomId) + } + + fun closeGameRx(roomId: Long): Single { + return api.closeGameRx(roomId) + .compose(RxHelper.handleBeanData()) + .compose(RxHelper.handleSchAndExce()) + } + + private interface Api { + /** + * 恢复游戏房间信息 + */ + @GET("miniGame/nav/resumeRoom") + fun getResumeGameRoom(): Single> + + /** + * 房间信息 + */ + @GET("chatRoom/getByType") + suspend fun getChatRoomInfo(@Query("roomType") roomType: Int): ServiceResult + + /** + * 首页游戏配置 + */ + @GET("miniGame/nav/config") + suspend fun getHomeGameConfig(): ServiceResult + + /** + * 开始游戏 + */ + @POST("/miniGame/nav/start") + @FormUrlEncoded + suspend fun startGame( + @Field("mgId") mgId: String, + @Field("gameMode") gameMode: Int + ): ServiceResult + + /** + * 关闭游戏 + */ + @GET("miniGame/nav/close") + suspend fun closeGame(@Query("roomId") roomId: Long): ServiceResult + + /** + * 关闭游戏 + */ + @GET("miniGame/nav/close") + fun closeGameRx(@Query("roomId") roomId: Long):Single> + } + +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/data/bean/GameInfoUiState.kt b/app/src/module_game/java/com/chwl/app/game/data/bean/GameInfoUiState.kt new file mode 100644 index 0000000..3cf9247 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/data/bean/GameInfoUiState.kt @@ -0,0 +1,15 @@ +package com.chwl.app.game.data.bean + +import com.chwl.core.bean.game.GameRoomInfo + + +sealed class GameInfoUiState { + + object Loading : GameInfoUiState() + + object Success : GameInfoUiState() + + data class Failed(val throwable: Throwable, val gameInfo: GameRoomInfo?) : GameInfoUiState() + + data class Empty(val gameInfo: GameRoomInfo?) : GameInfoUiState() +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/buy/GameBuyDialog.kt b/app/src/module_game/java/com/chwl/app/game/ui/buy/GameBuyDialog.kt new file mode 100644 index 0000000..0ad5e0c --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/buy/GameBuyDialog.kt @@ -0,0 +1,114 @@ +package com.chwl.app.game.ui.buy + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.chwl.app.R +import com.chwl.app.common.widget.dialog.DialogManager +import com.chwl.app.databinding.GameBuyDialogBinding +import com.chwl.app.game.ui.game.GameActivity +import com.chwl.app.game.ui.game.GameIntent +import com.chwl.app.game.ui.home.GameHomeViewModel +import com.chwl.app.ui.pay.ChargeActivity +import com.chwl.app.ui.webview.DialogWebViewActivity +import com.chwl.core.bean.game.GameConfigBean +import com.chwl.core.bean.game.GameModeBean +import com.chwl.core.utils.net.BalanceNotEnoughExeption +import com.chwl.library.utils.ResUtil +import com.chwl.library.utils.SingleToastUtil +import com.example.lib_utils.ktx.singleClick +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class GameBuyDialog(private val gameConfig: GameConfigBean, private val gameMode: GameModeBean) : + BottomSheetDialogFragment() { + + private var binding: GameBuyDialogBinding? = null + + private val viewModel: GameHomeViewModel by activityViewModels() + + private var dialogManager: DialogManager? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialogManager = DialogManager(requireContext()) + initView() + initObserver() + } + + private fun initView() { + binding?.tvTitle?.text = context?.getString(R.string.game_ticket_format, gameMode.modeName) + binding?.tvCoins?.text = gameMode.ticket?.toString() + binding?.ivHelp?.singleClick { + DialogWebViewActivity.start(context, gameMode.ruleUrl ?: "", true) + } + binding?.tvStart?.singleClick { + dialogManager?.showProgressDialog(requireContext()) + viewModel.startGame(gameConfig.mgIdStr ?: "", gameMode.gameMode ?: -1) + } + } + + private fun initObserver() { + viewLifecycleOwner.lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.startGameFlow.collectLatest { + dialogManager?.dismissDialog() + if (it.isSuccess) { + dismissAllowingStateLoss() + val intent = GameIntent(gameConfig.mgId ?: 0L, gameMode.gameMode ?: -1) + GameActivity.start(requireContext(), intent) + } else { + if (it.code == BalanceNotEnoughExeption.code) { + showBalanceNotEnoughDialog() + } else { + it.message?.let { msg -> + SingleToastUtil.showToast(msg) + } + } + } + } + } + } + } + + private fun showBalanceNotEnoughDialog() { + dialogManager?.showOkCancelDialog( + ResUtil.getString(R.string.widget_dialog_dialoguihelper_04), + ResUtil.getString(R.string.treasure_to_charge) + ) { + ChargeActivity.start(context) + } + } + + override fun onDestroyView() { + super.onDestroyView() + dialogManager?.dismissDialog() + dialogManager = null + } + + override fun getTheme(): Int { + return R.style.ErbanBottomSheetDialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = GameBuyDialogBinding.inflate(LayoutInflater.from(context)) + return binding?.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + this.setCanceledOnTouchOutside(true) + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/GameActivity.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/GameActivity.kt new file mode 100644 index 0000000..de25fdd --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/GameActivity.kt @@ -0,0 +1,153 @@ +package com.chwl.app.game.ui.game + +import android.content.Context +import android.content.Intent +import android.widget.FrameLayout +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.chwl.app.R +import com.chwl.app.common.widget.dialog.DialogManager.OkCancelDialogListener +import com.chwl.app.databinding.GameActivityBinding +import com.chwl.app.game.core.BaseGameActivity +import com.chwl.app.game.core.GameContext +import com.chwl.app.game.core.GameStateAbility +import com.chwl.app.game.ui.game.widgets.bottom.GameBottomWidget +import com.chwl.app.game.ui.game.widgets.message.GameMessageWidget +import com.chwl.app.game.ui.game.widgets.queue.GameQueueWidget +import com.chwl.app.ui.utils.loadImage +import com.chwl.core.sud.model.GameViewInfoModel +import com.chwl.core.support.room.RoomView +import com.chwl.library.common.util.doLog +import com.example.lib_utils.ktx.getColorById +import com.example.lib_utils.ktx.singleClick +import com.example.lib_utils.log.ILog +import com.example.lib_utils.spannable.SpannableTextBuilder +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class GameActivity : BaseGameActivity(), RoomView, ILog { + + companion object { + fun start(context: Context, intent: GameIntent) { + context.startActivity(Intent(context, GameActivity::class.java).apply { + putExtra("intent", intent) + }) + } + } + + override fun init() { + val intentData = intent.getSerializableExtra("intent") as? GameIntent + if (intentData == null) { + toast(R.string.utils_net_beanobserver_05) + finish() + return + } + super.init() + viewModel.init(intentData) + } + + override fun initView() { + super.initView() + } + + override fun initEvent() { + super.initEvent() + binding.ivClose.singleClick { + onBackPressed() + } + } + + override fun onGameContext(gameContext: GameContext) { + super.onGameContext(gameContext) + val stateAbility = + gameContext.findAbility(GameStateAbility::class.java.simpleName) + ?: return + lifecycleScope.launch { + stateAbility.gameIconFlow.collectLatest { + binding.ivLogo.loadImage(it) + } + } + + lifecycleScope.launch { + stateAbility.scoresFlow.collectLatest { + "飞行棋 游戏奖励钱币的变化 ${it}".doLog() + updateAward(it?.first()) + } + } + } + + override fun initGameEngine(gameContext: GameContext) { + binding.spaceGameRect.post { + val rect = GameViewInfoModel.GameViewRectModel().apply { + top = binding.spaceGameRect.top + bottom = binding.root.height - binding.spaceGameRect.bottom + } + logD("initGameEngine() height:${binding.root.height}") + logD("initGameEngine() top:${rect.top} bottom:${rect.bottom}") + gameEngineAbility?.setGameViewRect(rect) + } + super.initGameEngine(gameContext) + } + + private fun updateAward(number: Double?) { + val numberStr = number?.toInt()?.toString() ?: "" + if (numberStr.isEmpty()) { + binding.layoutAward.isInvisible = true + binding.tvAwardTips.isInvisible = true + } else { + binding.layoutAward.isVisible = true + binding.tvAwardTips.isVisible = true + binding.tvAwardValue.text = numberStr + val awardTips = getString(R.string.game_award_tips_format, numberStr) + SpannableTextBuilder(binding.tvAwardTips).appendText(awardTips) + .setTextStyle(numberStr, textColor = getColorById(R.color.color_FF6629)) + .setTextStyle(getString(R.string.game_award_tips_coins_text), textColor = getColorById(R.color.color_FF6629)) + .apply() + } + } + + override fun initObserver() { + super.initObserver() + } + + override fun initWidgets() { + super.initWidgets() + registerWidget(GameMessageWidget::class.java.simpleName, binding.messageWidget) + registerWidget(GameQueueWidget::class.java.simpleName, binding.queueWidget) + registerWidget(GameBottomWidget::class.java.simpleName, binding.bottomWidget) + } + + override fun onBackPressed() { +// super.onBackPressed() + showExitTips() + } + + override fun getGameViewGroup(): FrameLayout { + return binding.layoutGame + } + + private fun showExitTips() { + val stateAbility = + getRoomContext()?.findAbility(GameStateAbility::class.java.simpleName) + val message = if (stateAbility?.gameStateFlow?.value == GameStateAbility.STATE_MATCHING) { + getString(R.string.game_exit_tips_matching) + } else { + getString(R.string.game_exit_tips) + } + dialogManager.showOkCancelDialog(null, + message, + getString(R.string.continue_game), + getString(R.string.exit_text), false, false, object : OkCancelDialogListener { + override fun onOk() { + dialogManager.dismissDialog() + } + + override fun onCancel() { + super.onCancel() + loadingDialogManager?.showProgressDialog(this@GameActivity) + viewModel.closeRoom() + } + }) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/GameIntent.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/GameIntent.kt new file mode 100644 index 0000000..1a2fd64 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/GameIntent.kt @@ -0,0 +1,8 @@ +package com.chwl.app.game.ui.game + +import androidx.annotation.Keep +import java.io.Serializable + +@Keep +data class GameIntent(val gameId: Long, val gameMode: Int) : Serializable { +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/GameViewModel.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/GameViewModel.kt new file mode 100644 index 0000000..66983c7 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/GameViewModel.kt @@ -0,0 +1,49 @@ +package com.chwl.app.game.ui.game + +import androidx.lifecycle.MutableLiveData +import com.chwl.app.base.BaseViewModel +import com.chwl.app.game.core.GameContext +import com.chwl.app.game.data.GameModel2 +import com.chwl.core.bean.response.BeanResult +import kotlinx.coroutines.flow.MutableSharedFlow + +class GameViewModel : BaseViewModel() { + + val gameContextLiveData = MutableLiveData() + + val closeRoomFlow = MutableSharedFlow>() + + private var gameIntent: GameIntent? = null + + fun init(intent: GameIntent) { + this.gameIntent = intent + val gameContext = GameContext(0) + gameContext.performStart() + gameContextLiveData.value = gameContext + } + + fun getGameIntent(): GameIntent? { + return gameIntent + } + + override fun onCleared() { + super.onCleared() + gameContextLiveData.value?.performStop() + } + + fun closeRoom() { + val roomId = gameContextLiveData.value?.roomId + if (roomId != null) { + safeLaunch(onError = { + closeRoomFlow.emit(BeanResult.failed(it)) + }) { + GameModel2.closeGame(roomId) + closeRoomFlow.emit(BeanResult.success(null)) + } + } else { + safeLaunch { + closeRoomFlow.emit(BeanResult.success(null)) + } + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/input/GameMessageInputDialog.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/input/GameMessageInputDialog.kt new file mode 100644 index 0000000..4e1b814 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/input/GameMessageInputDialog.kt @@ -0,0 +1,95 @@ +package com.chwl.app.game.ui.game.input + +import android.app.Dialog +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.chwl.app.R +import com.chwl.app.common.widget.dialog.DialogManager +import com.chwl.app.databinding.GameMessageInputDialogBinding +import com.chwl.app.game.core.GameContext +import com.chwl.app.game.core.GameMessageAbility +import com.chwl.core.support.room.RoomContext +import com.chwl.library.utils.SingleToastUtil +import com.chwl.library.utils.keyboard.KeyboardUtil +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.launch + +class GameMessageInputDialog(private var onSend: ((GameMessageInputDialog, String) -> Unit)?) : + BottomSheetDialogFragment() { + + private var binding: GameMessageInputDialogBinding? = null + + private var dialogManager: DialogManager? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialogManager = DialogManager(requireContext()) + initView() + initObserver() + } + + private fun initView() { + binding?.inputEdit?.let { + KeyboardUtil.showKeyboardInDialog(this.dialog, it) + } + binding?.inputEdit?.setOnKeyListener { _, keyCode, event -> + if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP) { + send() + return@setOnKeyListener true + } + false + } + binding?.inputSend?.setOnClickListener { + send() + } + } + + private fun send() { + val text = binding?.inputEdit?.text?.toString()?.trim() + onSend?.invoke(this, text ?: "") + } + + private fun initObserver() { + viewLifecycleOwner.lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + dialogManager?.dismissDialog() + dialogManager = null + } + + override fun getTheme(): Int { + return R.style.ErbanBottomSheetDialogDimFalse + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = GameMessageInputDialogBinding.inflate(LayoutInflater.from(context)) + return binding?.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + this.setCanceledOnTouchOutside(true) + } + } + + override fun onDestroy() { + super.onDestroy() + onSend = null + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/bottom/GameBottomWidget.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/bottom/GameBottomWidget.kt new file mode 100644 index 0000000..c7c5dc7 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/bottom/GameBottomWidget.kt @@ -0,0 +1,74 @@ +package com.chwl.app.game.ui.game.widgets.bottom + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.databinding.DataBindingUtil +import com.chwl.app.R +import com.chwl.app.databinding.GameBottomWidgetBinding +import com.chwl.app.game.core.GameMessageAbility +import com.chwl.app.game.ui.game.input.GameMessageInputDialog +import com.chwl.core.support.room.FrameLayoutRoomWidget +import com.chwl.library.utils.ResUtil +import com.chwl.library.utils.SingleToastUtil + +class GameBottomWidget : FrameLayoutRoomWidget { + + private val binding: GameBottomWidgetBinding = + DataBindingUtil.inflate( + LayoutInflater.from( + context + ), R.layout.game_bottom_widget, this, true + ) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + init { + binding.tvInput.setOnClickListener { + roomView?.let { + roomContext?.let { context -> + GameMessageInputDialog { dialog, text -> + sendMessage(dialog, text) + }.show( + it.getViewFragmentManager(), + "MESSAGE_INPUT" + ) + } + } + } + } + + private fun sendMessage(dialog: GameMessageInputDialog, message: String) { + if (TextUtils.isEmpty(message)) { + SingleToastUtil.showToast(ResUtil.getString(R.string.avroom_fragment_baseroomfragment_08)) + return + } + val messageAbility = + roomContext?.findAbility(GameMessageAbility::class.java.simpleName) + if (messageAbility == null) { + dialog.dismissAllowingStateLoss() + SingleToastUtil.showToast(ResUtil.getString(R.string.retryTips)) + return + } + addDisposable(messageAbility.sendTextMessage(message).subscribe({ + dialog.dismissAllowingStateLoss() + }, { + dialog.dismissAllowingStateLoss() + SingleToastUtil.showToast(it.message) + })) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageAdapter.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageAdapter.kt new file mode 100644 index 0000000..1509794 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageAdapter.kt @@ -0,0 +1,224 @@ +package com.chwl.app.game.ui.game.widgets.message + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.text.style.ImageSpan +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.chwl.app.R +import com.chwl.app.common.widget.CustomImageSpan +import com.chwl.app.ui.utils.ImageLoadUtils +import com.chwl.app.ui.widget.TextSpannableBuilder +import com.chwl.core.auth.AuthModel +import com.chwl.core.level.UserLevelResourceType +import com.chwl.core.manager.AvRoomDataManager +import com.chwl.core.noble.NobleUtil +import com.chwl.core.user.bean.UserInfo +import com.chwl.library.common.util.Utils +import com.chwl.library.utils.ResUtil +import com.chwl.library.utils.SizeUtils +import com.example.lib_utils.AppUtils +import com.example.lib_utils.UiUtils.isRtl +import com.example.lib_utils.ktx.getColorById +import com.netease.nim.uikit.business.session.emoji.MoonUtil +import com.netease.nim.uikit.common.util.sys.ScreenUtil +import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage +import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum + +class GameMessageAdapter(val dataList: MutableList) : + RecyclerView.Adapter(), View.OnClickListener { + + private val itemPaddingHorizontal = Utils.dip2px(AppUtils.getApp(), 11f) + private val itemPaddingVertical = Utils.dip2px(AppUtils.getApp(), 6f) + private val expLevelHeight = Utils.dip2px(AppUtils.getApp(), 18f) + private val badgeWidth = Utils.dip2px(AppUtils.getApp(), 15f) + private val badgeHeight = Utils.dip2px(AppUtils.getApp(), 15f) + private val greyColor = ContextCompat.getColor(AppUtils.getApp(), R.color.white_transparent_50) + private val imageSpanFactory = object : Function1 { + override fun invoke(p1: Drawable): ImageSpan { + return CustomImageSpan(p1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameMessageNormalViewHolder { + return GameMessageNormalViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.game_message_widget_item_text, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) + val viewHolder = holder as GameMessageNormalViewHolder + viewHolder.textView.let { + it.setLineSpacing(0f, 1f) + it.setTextColor(Color.WHITE) + it.textSize = 12f + it.setOnClickListener(this) + it.setOnLongClickListener(null) + it.tag = item + it.gravity = Gravity.START or Gravity.CENTER_VERTICAL + if (isRtl(it.context)) { + it.textDirection = View.TEXT_DIRECTION_RTL + } + resetItemBackground(it) + } + if (item is ChatRoomMessage) { + if (item.msgType == MsgTypeEnum.text) { + convertText(viewHolder, item) + } else if (item.msgType == MsgTypeEnum.tip) { + convertTips(viewHolder, item) + } + } + } + + private fun convertTips(holder: GameMessageNormalViewHolder, message: ChatRoomMessage) { + val textView = holder.textView + textView.setTextColor(ContextCompat.getColor(textView.context, R.color.color_white)) + textView.text = message.content + textView.setBackgroundResource(R.drawable.shape_room_message_tip_bg) + } + + private fun convertText(holder: GameMessageNormalViewHolder, message: ChatRoomMessage) { + val textView = holder.textView + val text = TextSpannableBuilder(textView) + addUserTag(message, text, textView) + val nickName = + if (message.fromAccount != null && message.fromAccount == AuthModel.get().currentUid.toString() + "") { + ResUtil.getString(R.string.avroom_widget_messageview_0116) + } else { + NobleUtil.getNamePlate(UserInfo.NICK, message) + } + text.append(nickName, ForegroundColorSpan(greyColor)) + text.append( + ": " + message.content, + ForegroundColorSpan(textView.context.getColorById(R.color.white)) + ) + MoonUtil.replaceEmoticons( + textView.context, + text.builder.toString(), + text.builder, + imageSpanFactory + ) + textView.text = text.build() + loadItemBackgroundVip(message, textView) + } + + private fun addUserTag( + chatRoomMessage: ChatRoomMessage, + builder: TextSpannableBuilder, + textView: TextView + ) { + val extension = chatRoomMessage.chatRoomMessageExtension + val userLevel = NobleUtil.getLevel(UserLevelResourceType.EXPER_URL, chatRoomMessage) + val isOfficial = NobleUtil.getIsOfficial(UserInfo.IS_OFFICIAL, chatRoomMessage) + val vipIcon = NobleUtil.getResource(UserInfo.VIP_ICON, chatRoomMessage) + builder.append(vipIcon, expLevelHeight) + .append( + if (isOfficial) ResourcesCompat.getDrawable( + textView.resources, + R.mipmap.ic_user_official_13dp, null + ) else null, + badgeWidth, badgeHeight + ) + .append(getNewUserDrawable(textView.context, chatRoomMessage), badgeWidth, badgeHeight) + .append( + if (AvRoomDataManager.get() + .isSuperAdmin(chatRoomMessage.fromAccount) + ) ResourcesCompat.getDrawable( + textView.resources, + R.drawable.ic_room_super_admin, null + ) else null, + SizeUtils.dp2px(textView.context, 23f), expLevelHeight + ) + + // 官方主播認證 + val tvOfficialMask = + NobleUtil.getLevel(UserInfo.OAC_NAME, chatRoomMessage).trim { it <= ' ' } + val ivOfficialMask = NobleUtil.getLevel(UserInfo.OAC_ICON, chatRoomMessage) + if (!TextUtils.isEmpty(tvOfficialMask) && !TextUtils.isEmpty(ivOfficialMask) && extension != null) { // extension != null 表示自己 + builder.appendBgAndContent(ivOfficialMask, tvOfficialMask) + } else if (!TextUtils.isEmpty(ivOfficialMask)) { + builder.append(ivOfficialMask, SizeUtils.dp2px(textView.context, 62f), expLevelHeight) + } + //等級 + builder.append(userLevel, expLevelHeight) + //銘牌 + val tvNamePlate = + NobleUtil.getNamePlate(UserInfo.NAMEPLATE_WORD, chatRoomMessage).trim { it <= ' ' } + val ivNamePlate = NobleUtil.getNamePlate(UserInfo.NAMEPLATE_PIC, chatRoomMessage) + if (!TextUtils.isEmpty(tvNamePlate) && !TextUtils.isEmpty(ivNamePlate)) { // extension != null 表示自己 + builder.appendBgAndContent(ivNamePlate, tvNamePlate) + } else if (!TextUtils.isEmpty(ivNamePlate)) { + builder.append(ivNamePlate, expLevelHeight) + } + } + + private fun getNewUserDrawable(context: Context, chatRoomMessage: ChatRoomMessage): Drawable? { + val newUser = NobleUtil.getIsNewUser(UserInfo.IS_NEW_USER, chatRoomMessage) + val isHelloUser = + NobleUtil.getIsNewUser(UserInfo.IS_FROM_SAY_HELLO_CHANNEL, chatRoomMessage) + return if (newUser) { + ResourcesCompat.getDrawable( + context.resources, + if (isHelloUser) R.drawable.ic_new_user_hello else R.drawable.ic_new_user, + null + ) + } else null + } + + private fun resetItemBackground(textView: TextView) { + textView.text = "" + textView.setBackgroundResource(R.drawable.shape_room_message_bg) + textView.setPadding( + itemPaddingHorizontal, + itemPaddingVertical, + itemPaddingHorizontal, + itemPaddingVertical + ) + } + + private fun loadItemBackgroundVip(chatRoomMessage: ChatRoomMessage?, view: View) { + val androidBubbleUrl = NobleUtil.getResource(UserInfo.BUBBLE_URL_ANDROID, chatRoomMessage) + if (TextUtils.isEmpty(androidBubbleUrl)) return + view.setPadding( + itemPaddingHorizontal, + ScreenUtil.dip2px(10f), + itemPaddingHorizontal, + ScreenUtil.dip2px(10f) + ) + ImageLoadUtils.loadNinePatchBg(view, androidBubbleUrl) + } + + fun setDataList(list: List) { + dataList.clear() + dataList.addAll(list) + notifyDataSetChanged() + } + + fun addData(item: Any) { + dataList.add(item) + notifyItemInserted(dataList.size) + } + + fun getItem(position: Int): Any? { + return dataList.get(position) + } + + override fun getItemCount(): Int { + return dataList.size + } + + override fun onClick(v: View) { + + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageNormalViewHolder.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageNormalViewHolder.kt new file mode 100644 index 0000000..af73856 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageNormalViewHolder.kt @@ -0,0 +1,12 @@ +package com.chwl.app.game.ui.game.widgets.message + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.chwl.app.R + +class GameMessageNormalViewHolder(val itemView: View) : RecyclerView.ViewHolder(itemView) { + + var textView: TextView = itemView.findViewById(R.id.tv_content) + +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageWidget.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageWidget.kt new file mode 100644 index 0000000..6bed89d --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/message/GameMessageWidget.kt @@ -0,0 +1,121 @@ +package com.chwl.app.game.ui.game.widgets.message + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.chwl.app.R +import com.chwl.app.game.core.GameMessageAbility +import com.chwl.app.ui.widget.DividerItemDecoration +import com.chwl.app.ui.widget.MyItemAnimator +import com.chwl.app.ui.widget.RecyclerViewNoViewpagerScroll +import com.chwl.core.support.room.FrameLayoutRoomWidget +import com.chwl.core.support.room.RoomContext +import com.chwl.core.support.room.RoomView + +class GameMessageWidget : FrameLayoutRoomWidget, GameMessageAbility.Listener { + + private val recyclerView = RecyclerViewNoViewpagerScroll(context) + private val adapter = GameMessageAdapter(ArrayList()) + private var isAtBottom = true + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + init { + initListView() + initEvent() + } + + private fun initListView() { + recyclerView.layoutParams = + ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + recyclerView.setFadingEdgeLength(60) + recyclerView.isVerticalFadingEdgeEnabled = true + recyclerView.overScrollMode = OVER_SCROLL_NEVER + recyclerView.isHorizontalScrollBarEnabled = false + addView(recyclerView) + val layoutManger = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + recyclerView.layoutManager = layoutManger + recyclerView.addItemDecoration( + DividerItemDecoration( + context, + layoutManger.orientation, + 16, + R.color.transparent + ) + ) + recyclerView.itemAnimator = MyItemAnimator().apply { + this.supportsChangeAnimations = false + this.addDuration = 0 + this.changeDuration = 0 + this.moveDuration = 0 + this.removeDuration = 0 + } + recyclerView.adapter = adapter + } + + private fun initEvent() { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + val lastPosition = layoutManager.findLastVisibleItemPosition() + if (lastPosition == RecyclerView.NO_POSITION) { + isAtBottom = true + } else isAtBottom = lastPosition == adapter.itemCount - 1 + } + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (dy < 0) { + isAtBottom = false + } + } + }) + } + + override fun onInitialize(roomView: RoomView, roomContext: RoomContext) { + super.onInitialize(roomView, roomContext) + val messageAbility = + roomContext.findAbility(GameMessageAbility::class.java.simpleName) + messageAbility?.getMessageList()?.let { + adapter.setDataList(it) + } + messageAbility?.addListener(this) + } + + override fun onAddMessage(message: Any) { + adapter.addData(message) + tryScrollToBottom() + } + + override fun onReplaceMessageList(list: List) { + adapter.setDataList(list) + tryScrollToBottom() + } + + private fun tryScrollToBottom() { + logD("tryScrollToBottom isAtBottom:$isAtBottom") + if (isAtBottom) { + if (adapter.itemCount > 0) { + recyclerView.smoothScrollToPosition(adapter.itemCount - 1) + } + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/queue/GameQueueWidget.kt b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/queue/GameQueueWidget.kt new file mode 100644 index 0000000..23df34f --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/game/widgets/queue/GameQueueWidget.kt @@ -0,0 +1,96 @@ +package com.chwl.app.game.ui.game.widgets.queue + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.databinding.DataBindingUtil +import com.chwl.app.R +import com.chwl.app.databinding.GameQueueWidgetBinding +import com.chwl.app.game.core.GameQueueAbility +import com.chwl.app.game.core.GameStateAbility +import com.chwl.app.ui.utils.loadAvatar +import com.chwl.core.auth.AuthModel +import com.chwl.core.bean.room.RoomMicBean +import com.chwl.core.support.room.FrameLayoutRoomWidget +import com.chwl.core.support.room.RoomContext +import com.chwl.core.support.room.RoomView +import kotlinx.coroutines.flow.collectLatest + +class GameQueueWidget : FrameLayoutRoomWidget { + + private val binding: GameQueueWidgetBinding = + DataBindingUtil.inflate( + LayoutInflater.from( + context + ), R.layout.game_queue_widget, this, true + ) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onInitialize(roomView: RoomView, roomContext: RoomContext) { + super.onInitialize(roomView, roomContext) + val stateAbility = + roomContext.findAbility(GameStateAbility::class.java.simpleName) + val queueAbility = + roomContext.findAbility(GameQueueAbility::class.java.simpleName) + queueAbility?.let { + safeLaunch { + it.queueFlow.collectLatest { + updateQueue(it) + } + } + } + stateAbility?.let { + safeLaunch { + it.gameStateFlow.collectLatest { + updateState(it) + } + } + } + } + + private fun updateQueue(list: List?) { + val selfInfo = list?.firstOrNull { + it.micUser?.uid == AuthModel.get().currentUid + } + + val targetInfo = list?.firstOrNull { + it.micUser?.uid != AuthModel.get().currentUid + } + + binding.ivQueue1.loadAvatar(selfInfo?.micUser?.avatar) + binding.ivQueue2.loadAvatar(targetInfo?.micUser?.avatar) + } + + private fun updateState(gameState: Int?) { + when (gameState) { + GameStateAbility.STATE_MATCH_SUCCESS, GameStateAbility.STATE_GAME_END -> { + binding.tvState.setText(R.string.match_successfully) + binding.ivState.setImageResource(R.drawable.game_ic_vs) + } + + GameStateAbility.STATE_MATCH_FAILED -> { + binding.tvState.setText(R.string.match_failed) + binding.ivState.setImageResource(R.drawable.game_ic_link) + } + + else -> { + binding.tvState.setText(R.string.matchmaking) + binding.ivState.setImageResource(R.drawable.game_ic_link) + } + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeAdapter.kt b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeAdapter.kt new file mode 100644 index 0000000..1fe8ae3 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeAdapter.kt @@ -0,0 +1,14 @@ +package com.chwl.app.game.ui.home + +import android.widget.ImageView +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.BaseViewHolder +import com.chwl.app.R +import com.chwl.core.bean.game.GameModeBean +import com.chwl.app.ui.utils.load + +class GameHomeAdapter : BaseQuickAdapter(R.layout.game_home_item) { + override fun convert(helper: BaseViewHolder, item: GameModeBean?) { + helper.getView(R.id.iv_game).load(item?.modeIcon) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeFragment.kt b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeFragment.kt new file mode 100644 index 0000000..6eddaf7 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeFragment.kt @@ -0,0 +1,139 @@ +package com.chwl.app.game.ui.home + +import androidx.fragment.app.activityViewModels +import com.chwl.app.MainTabContentView +import com.chwl.app.base.BaseViewBindingFragment +import com.chwl.app.databinding.GameHomeFragmentBinding +import com.chwl.app.game.ui.buy.GameBuyDialog +import com.chwl.app.support.FragmentVisibleStateHelper +import com.chwl.app.ui.pay.ChargeActivity +import com.chwl.app.ui.utils.loadAvatar +import com.chwl.app.ui.webview.CommonWebViewActivity +import com.chwl.app.ui.widget.recyclerview.decoration.GridSpacingItemNewDecoration +import com.chwl.app.utils.HomeUIManager +import com.chwl.core.UriProvider +import com.chwl.core.bean.game.GameConfigBean +import com.chwl.core.pay.PayModel +import com.chwl.core.pay.event.GetWalletInfoEvent +import com.chwl.core.pay.event.UpdateWalletInfoEvent +import com.chwl.core.user.UserModel +import com.chwl.core.user.event.LoginUserInfoUpdateEvent +import com.chwl.core.utils.CoreTextUtils +import com.chwl.library.utils.FormatUtils +import com.example.lib_utils.UiUtils +import com.example.lib_utils.ktx.singleClick +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + + +class GameHomeFragment : BaseViewBindingFragment(), MainTabContentView { + private val adapter = GameHomeAdapter() + private val viewModel: GameHomeViewModel by activityViewModels() + override fun init() { + EventBus.getDefault().register(this) + initView() + initEvent() + initObserver() + FragmentVisibleStateHelper(this).start { + onVisibleChanged(it) + } + PayModel.get().refreshWalletInfo(true) + refreshWalletInfo() + refreshUserInfo() + } + + private fun initView() { + HomeUIManager.setHomeUI(binding.root) + binding.recyclerView.addItemDecoration( + GridSpacingItemNewDecoration( + UiUtils.dip2px(10f), + UiUtils.dip2px(18.5f), + false + ) + ) + binding.recyclerView.adapter = adapter + } + + private fun initEvent() { + binding.layoutWallet.singleClick { + ChargeActivity.start(context) + } + binding.ivRank.singleClick { + CommonWebViewActivity.start(requireContext(), UriProvider.getGameRank()) + } + adapter.setOnItemClickListener { _, view, position -> + val config = viewModel.gameConfigLiveData.value?.data + val gameMode = adapter.getItem(position) + if (config == null || gameMode == null) { + return@setOnItemClickListener + } + GameBuyDialog(config, gameMode).show(childFragmentManager, "GAME_BUY") + } + } + + private fun initObserver() { + viewModel.gameConfigLiveData.observe(this) { + if (it == null) { + return@observe + } + val configData = it.data + if (it.isSuccess && configData != null) { + loadGameInfo(configData) + } else { + toast(it.message) + } + } + } + + private fun onVisibleChanged(isVisible: Boolean) { + if (isVisible) { + if (viewModel.gameConfigLiveData.value?.isSuccess != true) { + viewModel.getGameList() + } + } + } + + private fun loadGameInfo(config: GameConfigBean) { +// binding.ivGameLogo.load(config.pic) + adapter.setNewData(config.gameModes) + } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onWalletInfoUpdate(event: UpdateWalletInfoEvent?) { + if (_binding == null) { + return + } + refreshWalletInfo() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onGetWalletInfoEvent(event: GetWalletInfoEvent?) { + if (_binding == null) { + return + } + refreshWalletInfo() + } + + private fun refreshWalletInfo() { + binding.tvWalletValue.text = + FormatUtils.formatBigInteger(PayModel.get().currentWalletInfo?.diamondNum ?: 0.0) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLoginUserInfoUpdateEvent(event: LoginUserInfoUpdateEvent?) { + if (_binding == null) { + return + } + refreshUserInfo() + } + + private fun refreshUserInfo() { + val userInfo = UserModel.get().cacheLoginUserInfo + binding.ivAvatar.loadAvatar(if (CoreTextUtils.isEmptyText(userInfo?.newAvatar)) userInfo?.avatar else userInfo?.newAvatar) + } + + override fun onDestroy() { + super.onDestroy() + EventBus.getDefault().unregister(this) + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeViewModel.kt b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeViewModel.kt new file mode 100644 index 0000000..23d8786 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/home/GameHomeViewModel.kt @@ -0,0 +1,33 @@ +package com.chwl.app.game.ui.home + +import androidx.lifecycle.MutableLiveData +import com.chwl.app.base.BaseViewModel +import com.chwl.app.game.data.GameModel2 +import com.chwl.core.bean.game.GameConfigBean +import com.chwl.core.bean.response.BeanResult +import com.chwl.core.pay.PayModel +import kotlinx.coroutines.flow.MutableSharedFlow + +class GameHomeViewModel : BaseViewModel() { + val gameConfigLiveData = MutableLiveData?>() + + val startGameFlow = MutableSharedFlow>() + + fun getGameList() { + safeLaunch(onError = { + gameConfigLiveData.postValue(BeanResult.Companion.failed(it)) + }) { + val configBean = GameModel2.getHomeGameConfig() + gameConfigLiveData.postValue(BeanResult.success(configBean)) + } + } + + fun startGame(gameId: String, gameMode: Int) { + safeLaunch(onError = { + startGameFlow.emit(BeanResult.Companion.failed(it)) + }) { + GameModel2.startGame(gameId, gameMode) + startGameFlow.emit(BeanResult.success(null)) + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultAdapter.kt b/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultAdapter.kt new file mode 100644 index 0000000..9be3dac --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultAdapter.kt @@ -0,0 +1,44 @@ +package com.chwl.app.game.ui.result + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.BaseViewHolder +import com.chwl.app.R +import com.chwl.core.bean.game.GameResultBean +import com.chwl.app.ui.utils.loadAvatar + +class GameResultAdapter : + BaseQuickAdapter(R.layout.game_result_item) { + override fun convert(helper: BaseViewHolder, item: GameResultBean?) { + helper.setText(R.id.tv_name, item?.nick ?: "") + helper.setText(R.id.tv_coins, item?.getScoreStr() ?: "0") + helper.getView(R.id.iv_user_avatar).loadAvatar(item?.avatar) + val rank = item?.rank ?: (helper.bindingAdapterPosition + 1) + val rankView = helper.getView(R.id.tv_rank) + val rootView = helper.getView(R.id.layout_root) + val coinsBgView = helper.getView(R.id.iv_coins_bg) + val avatarBorderView = helper.getView(R.id.iv_user_avatar_border) + if (rank == 1) { + avatarBorderView.setBackgroundResource(R.drawable.game_result_avatar_border_top_1) + rankView.setBackgroundResource(R.drawable.game_result_ic_top1) + rankView.text = "" + } else if (rank == 2) { + avatarBorderView.setBackgroundResource(R.drawable.game_result_avatar_border_top_2) + rankView.setBackgroundResource(R.drawable.game_result_ic_top2) + rankView.text = "" + } else { + avatarBorderView.background = null + rankView.background = null + rankView.text = "$rank" + } + if (rank % 2 == 1) { + rootView.setBackgroundResource(R.drawable.game_result_item_bg_top1) + coinsBgView.setBackgroundResource(R.drawable.game_result_coins_top1) + } else { + rootView.setBackgroundResource(R.drawable.game_result_item_bg_top2) + coinsBgView.setBackgroundResource(R.drawable.game_result_coins_top2) + } + } +} \ No newline at end of file diff --git a/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultDialog.kt b/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultDialog.kt new file mode 100644 index 0000000..bcbdbd5 --- /dev/null +++ b/app/src/module_game/java/com/chwl/app/game/ui/result/GameResultDialog.kt @@ -0,0 +1,93 @@ +package com.chwl.app.game.ui.result + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.fragment.app.DialogFragment +import com.alibaba.fastjson2.JSON +import com.chwl.app.R +import com.chwl.app.databinding.GameResultDialogBinding +import com.chwl.app.ui.widget.recyclerview.decoration.SpacingDecoration +import com.chwl.core.auth.AuthModel +import com.chwl.core.bean.game.GameResultBean +import com.chwl.library.common.util.Utils +import com.example.lib_utils.ktx.singleClick +import com.example.lib_utils.log.ILog +import com.example.lib_utils.log.LogUtil + +class GameResultDialog( + private val list: List, + private var onClose: (() -> Unit)?, + private var onRestart: (() -> Unit)? +) : DialogFragment(), ILog { + + private var binding: GameResultDialogBinding? = null + + private val adapter = GameResultAdapter() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initView() + adapter.setNewData(list.sortedBy { it.rank }) + val isWin = list.firstOrNull { + it.uid == AuthModel.get().currentUid + }?.isWin + if (isWin == true) { + binding?.ivState?.setImageResource(R.drawable.game_result_bg_win) + } else { + binding?.ivState?.setImageResource(R.drawable.game_result_bg_fail) + } + } + + private fun initView() { + LogUtil.d(" 游戏结束 弹窗 ${JSON.toJSONString(list)}") + binding?.tvClose?.singleClick { + onClose?.invoke() + } + binding?.tvRestart?.singleClick { + onRestart?.invoke() + } + binding?.recyclerView?.addItemDecoration( + SpacingDecoration( + 0, + Utils.dip2px(context, 10f), + false + ) + ) + binding?.recyclerView?.adapter = adapter + } + + + override fun onDestroyView() { + super.onDestroyView() + onClose = null + onRestart = null + } + + override fun getTheme(): Int { + return R.style.full_screen_dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = GameResultDialogBinding.inflate(LayoutInflater.from(context)) + return binding?.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + this.setCancelable(false) + this.setCanceledOnTouchOutside(false) + window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT + ) + } + } +} \ No newline at end of file diff --git a/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_fail.webp b/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_fail.webp new file mode 100644 index 0000000..cf45e12 Binary files /dev/null and b/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_fail.webp differ diff --git a/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_win.webp b/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_win.webp new file mode 100644 index 0000000..4671ff5 Binary files /dev/null and b/app/src/module_game/res/drawable-ar-xxhdpi/game_result_bg_win.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_bg.webp b/app/src/module_game/res/drawable-xxhdpi/game_bg.webp new file mode 100644 index 0000000..ca25915 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_bg.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_buy_bg.webp b/app/src/module_game/res/drawable-xxhdpi/game_buy_bg.webp new file mode 100644 index 0000000..ce9c05a Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_buy_bg.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_buy_bg_body.webp b/app/src/module_game/res/drawable-xxhdpi/game_buy_bg_body.webp new file mode 100644 index 0000000..a4c7ed2 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_buy_bg_body.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_buy_btn.webp b/app/src/module_game/res/drawable-xxhdpi/game_buy_btn.webp new file mode 100644 index 0000000..78ca583 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_buy_btn.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_buy_ic_help.webp b/app/src/module_game/res/drawable-xxhdpi/game_buy_ic_help.webp new file mode 100644 index 0000000..5fac00d Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_buy_ic_help.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_home_ic_add.webp b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_add.webp new file mode 100644 index 0000000..620a189 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_add.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_home_ic_avatar_border.webp b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_avatar_border.webp new file mode 100644 index 0000000..4acaed0 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_avatar_border.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_home_ic_rank.webp b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_rank.webp new file mode 100644 index 0000000..765c60e Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_home_ic_rank.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_ic_close.webp b/app/src/module_game/res/drawable-xxhdpi/game_ic_close.webp new file mode 100644 index 0000000..66526a8 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_ic_close.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_ic_coins.webp b/app/src/module_game/res/drawable-xxhdpi/game_ic_coins.webp new file mode 100644 index 0000000..600b65c Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_ic_coins.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_ic_link.webp b/app/src/module_game/res/drawable-xxhdpi/game_ic_link.webp new file mode 100644 index 0000000..b37017e Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_ic_link.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_ic_queue_avatar_border.webp b/app/src/module_game/res/drawable-xxhdpi/game_ic_queue_avatar_border.webp new file mode 100644 index 0000000..fb36d9a Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_ic_queue_avatar_border.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_ic_vs.webp b/app/src/module_game/res/drawable-xxhdpi/game_ic_vs.webp new file mode 100644 index 0000000..75c6255 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_ic_vs.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_1.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_1.webp new file mode 100644 index 0000000..2f55577 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_1.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_2.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_2.webp new file mode 100644 index 0000000..7f1caf8 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_avatar_border_top_2.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_bg_close.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_close.webp new file mode 100644 index 0000000..e9a127f Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_close.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_bg_fail.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_fail.webp new file mode 100644 index 0000000..0d8462f Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_fail.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_bg_restart.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_restart.webp new file mode 100644 index 0000000..7693519 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_restart.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_bg_win.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_win.webp new file mode 100644 index 0000000..48798c7 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_bg_win.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top1.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top1.webp new file mode 100644 index 0000000..b821f63 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top1.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top2.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top2.webp new file mode 100644 index 0000000..678dd40 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_ic_top2.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top1.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top1.webp new file mode 100644 index 0000000..2300241 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top1.webp differ diff --git a/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top2.webp b/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top2.webp new file mode 100644 index 0000000..753ae41 Binary files /dev/null and b/app/src/module_game/res/drawable-xxhdpi/game_result_item_bg_top2.webp differ diff --git a/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_fail.webp b/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_fail.webp new file mode 100644 index 0000000..a3e2361 Binary files /dev/null and b/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_fail.webp differ diff --git a/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_win.webp b/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_win.webp new file mode 100644 index 0000000..2f863d1 Binary files /dev/null and b/app/src/module_game/res/drawable-zh-rTW-xxhdpi/game_result_bg_win.webp differ diff --git a/app/src/module_game/res/drawable/game_bg_award.xml b/app/src/module_game/res/drawable/game_bg_award.xml new file mode 100644 index 0000000..6ea89fd --- /dev/null +++ b/app/src/module_game/res/drawable/game_bg_award.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_bg_bottom.xml b/app/src/module_game/res/drawable/game_bg_bottom.xml new file mode 100644 index 0000000..8b807d6 --- /dev/null +++ b/app/src/module_game/res/drawable/game_bg_bottom.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_home_bg.xml b/app/src/module_game/res/drawable/game_home_bg.xml new file mode 100644 index 0000000..1bc5526 --- /dev/null +++ b/app/src/module_game/res/drawable/game_home_bg.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_home_bg_wallet.xml b/app/src/module_game/res/drawable/game_home_bg_wallet.xml new file mode 100644 index 0000000..95fdf0b --- /dev/null +++ b/app/src/module_game/res/drawable/game_home_bg_wallet.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_result_bg_1.xml b/app/src/module_game/res/drawable/game_result_bg_1.xml new file mode 100644 index 0000000..457b1a5 --- /dev/null +++ b/app/src/module_game/res/drawable/game_result_bg_1.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_result_bg_2.xml b/app/src/module_game/res/drawable/game_result_bg_2.xml new file mode 100644 index 0000000..7c40019 --- /dev/null +++ b/app/src/module_game/res/drawable/game_result_bg_2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_result_coins_top1.xml b/app/src/module_game/res/drawable/game_result_coins_top1.xml new file mode 100644 index 0000000..60bd8f7 --- /dev/null +++ b/app/src/module_game/res/drawable/game_result_coins_top1.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/drawable/game_result_coins_top2.xml b/app/src/module_game/res/drawable/game_result_coins_top2.xml new file mode 100644 index 0000000..985bdfa --- /dev/null +++ b/app/src/module_game/res/drawable/game_result_coins_top2.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_activity.xml b/app/src/module_game/res/layout/game_activity.xml new file mode 100644 index 0000000..9967230 --- /dev/null +++ b/app/src/module_game/res/layout/game_activity.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_bottom_widget.xml b/app/src/module_game/res/layout/game_bottom_widget.xml new file mode 100644 index 0000000..894bd2f --- /dev/null +++ b/app/src/module_game/res/layout/game_bottom_widget.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_buy_dialog.xml b/app/src/module_game/res/layout/game_buy_dialog.xml new file mode 100644 index 0000000..b7cedcd --- /dev/null +++ b/app/src/module_game/res/layout/game_buy_dialog.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_home_fragment.xml b/app/src/module_game/res/layout/game_home_fragment.xml new file mode 100644 index 0000000..2a3689f --- /dev/null +++ b/app/src/module_game/res/layout/game_home_fragment.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_home_item.xml b/app/src/module_game/res/layout/game_home_item.xml new file mode 100644 index 0000000..9061b94 --- /dev/null +++ b/app/src/module_game/res/layout/game_home_item.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_message_input_dialog.xml b/app/src/module_game/res/layout/game_message_input_dialog.xml new file mode 100644 index 0000000..5fa6150 --- /dev/null +++ b/app/src/module_game/res/layout/game_message_input_dialog.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/module_game/res/layout/game_message_widget_item_text.xml b/app/src/module_game/res/layout/game_message_widget_item_text.xml new file mode 100644 index 0000000..648b52b --- /dev/null +++ b/app/src/module_game/res/layout/game_message_widget_item_text.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_queue_widget.xml b/app/src/module_game/res/layout/game_queue_widget.xml new file mode 100644 index 0000000..e9f39a9 --- /dev/null +++ b/app/src/module_game/res/layout/game_queue_widget.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_result_dialog.xml b/app/src/module_game/res/layout/game_result_dialog.xml new file mode 100644 index 0000000..b5ee757 --- /dev/null +++ b/app/src/module_game/res/layout/game_result_dialog.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/layout/game_result_item.xml b/app/src/module_game/res/layout/game_result_item.xml new file mode 100644 index 0000000..b2444e5 --- /dev/null +++ b/app/src/module_game/res/layout/game_result_item.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_game/res/values-ar/strings.xml b/app/src/module_game/res/values-ar/strings.xml new file mode 100644 index 0000000..9f92d11 --- /dev/null +++ b/app/src/module_game/res/values-ar/strings.xml @@ -0,0 +1,17 @@ + + + جاري المطابقة + تمت المطابقة + فشلت المطابقة + عملة ذهبية + الفوز بمكافأة %s عملة ذهبية + ابدأ + (%s)رسوم الدخول + لقد بدأت اللعبة. سيؤدي الخروج من الغرفة إلى فشل اللعبة. هل أنت متأكد من الخروج من الغرفة؟ + جاري المطابقة، الخروج من الغرفة ،نهاية المطابقة. هل تريد الخروج؟ + فشلت المطابقة، هل تريد المحاولة مرة أخرى؟ + إعادة المطابقة + جاري التحميل، هل تريد العودة إلى الغرفة؟ + انتهت اللعبة~ + متابعة اللعبة + \ No newline at end of file diff --git a/app/src/module_game/res/values-tr/strings.xml b/app/src/module_game/res/values-tr/strings.xml new file mode 100644 index 0000000..12973e1 --- /dev/null +++ b/app/src/module_game/res/values-tr/strings.xml @@ -0,0 +1,19 @@ + + + + Eşleştirme + Eşleştirme başarılı + Eşleştirme başarısız + Altın + %s altın kazandınız + Başlat + Giriş ücreti(%s) + Oyun başladı, odadan çıkarsanız oyunu kaybedersiniz, çıkmak istediğinize emin misiniz? + Oyun eşleştirme aşamasında, odadan çıkarsanız eşleştirme sona erecek, çıkmak istiyor musunuz? + Eşleştirme başarısız, tekrar denemek ister misiniz? + Rövanş + Oyun devam ediyor, odasına geri dönmek istiyor musunuz? + Oyun sona erdi~ + Oyuna devam et + + \ No newline at end of file diff --git a/app/src/module_game/res/values-zh-rTW/strings.xml b/app/src/module_game/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..0ea3529 --- /dev/null +++ b/app/src/module_game/res/values-zh-rTW/strings.xml @@ -0,0 +1,18 @@ + + + + 匹配中 + 匹配成功 + 匹配失敗 + 金幣 + 獲勝獎勵%s金幣 + 開始 + 入场费(%s) + 遊戲已經開始,退出房間將默認遊戲失敗,確認退出房間? + 遊戲匹配中,退出房間則結束匹配,是否退出? + 匹配失敗,是否重新開始? + 重新匹配 + 遊戲進行中,是否返回房間? + 遊戲已結束~ + 繼續遊戲 + \ No newline at end of file diff --git a/app/src/module_game/res/values/colors.xml b/app/src/module_game/res/values/colors.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/app/src/module_game/res/values/colors.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/module_game/res/values/strings.xml b/app/src/module_game/res/values/strings.xml new file mode 100644 index 0000000..7bd579f --- /dev/null +++ b/app/src/module_game/res/values/strings.xml @@ -0,0 +1,17 @@ + + + Matching + Match successful + Match failed + gold coins + Winning reward %s gold coins + Start + Admission Fee (%s) + The game has started. Exiting the room will be considered a game failure. Are you sure you want to exit the room? + During a match, exiting a room will end the match. Do you want to exit? + Match failed, do you want to rematch? + Rematch + Game in progress, do you want to return to the room? + The game is over ~ + Continue + \ No newline at end of file